@hardfin/cli 0.0.2-dev.15 → 0.0.2-dev.16
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 +84 -66
- package/dist/cli.js +49 -35
- 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 \
|
|
@@ -118,10 +118,10 @@ what the api-spec bridge publishes. Commit the document and the generated comman
|
|
|
118
118
|
|
|
119
119
|
Two checks keep the pair honest.
|
|
120
120
|
|
|
121
|
-
| Check
|
|
122
|
-
|
|
|
121
|
+
| Check | Refuses |
|
|
122
|
+
| ------------- | ----------------------------------------------------------------------------- |
|
|
123
123
|
| The generator | a document whose `info.version` is not a date, which means an earlier release |
|
|
124
|
-
| CI
|
|
124
|
+
| CI | a vendored document that was updated without regenerating the commands |
|
|
125
125
|
|
|
126
126
|
The first one matters because `hardfinhq/api-spec` can sit a release behind the monorepo
|
|
127
127
|
while its bridge pull request is open. Generating from that document would replace the
|
|
@@ -151,9 +151,9 @@ so CI and an agent sandbox need no browser.
|
|
|
151
151
|
|
|
152
152
|
While it waits, the terminal takes two things.
|
|
153
153
|
|
|
154
|
-
| Key
|
|
155
|
-
|
|
|
156
|
-
| `c`
|
|
154
|
+
| Key | Does |
|
|
155
|
+
| -------------------------- | ----------------------------------------- |
|
|
156
|
+
| `c` | Copies the URL to the clipboard |
|
|
157
157
|
| A pasted value, then enter | Finishes the sign in without the listener |
|
|
158
158
|
|
|
159
159
|
Paste whichever of these you have: the whole redirect URL from the browser's address bar,
|
|
@@ -190,9 +190,9 @@ nothing about whether the credential is good.
|
|
|
190
190
|
|
|
191
191
|
### Where the refresh token is kept
|
|
192
192
|
|
|
193
|
-
| Host
|
|
194
|
-
|
|
|
195
|
-
| macOS, Windows, and Linux with a secret service
|
|
193
|
+
| Host | Kept in |
|
|
194
|
+
| --------------------------------------------------------------- | ----------------------------------------------------- |
|
|
195
|
+
| macOS, Windows, and Linux with a secret service | the OS keyring |
|
|
196
196
|
| Everything else, including WSL, containers, and agent sandboxes | `$XDG_STATE_HOME/hardfin/credentials.json`, mode 0600 |
|
|
197
197
|
|
|
198
198
|
`HARDFIN_NO_BROWSER=1` keeps `hardfin login` from opening a tab, which is what a script or a
|
|
@@ -210,10 +210,10 @@ production never share a sign in.
|
|
|
210
210
|
Nothing. Hardfin registers this CLI as a first-party client, and its key is the same in every
|
|
211
211
|
environment, so `hardfin login` works out of the box.
|
|
212
212
|
|
|
213
|
-
| Setting
|
|
214
|
-
|
|
|
215
|
-
| `clientId`, or `HARDFIN_CLIENT_ID`
|
|
216
|
-
| `issuerUrl`, or `HARDFIN_ISSUER_URL` | the authorization server
|
|
213
|
+
| Setting | Holds | Default |
|
|
214
|
+
| ------------------------------------ | ----------------------------------- | ----------------------------- |
|
|
215
|
+
| `clientId`, or `HARDFIN_CLIENT_ID` | the client this CLI names itself as | the seeded Hardfin CLI client |
|
|
216
|
+
| `issuerUrl`, or `HARDFIN_ISSUER_URL` | the authorization server | the API's host root |
|
|
217
217
|
|
|
218
218
|
## Completing commands in your shell
|
|
219
219
|
|
|
@@ -244,11 +244,11 @@ claude mcp add hardfin -- hardfin mcp
|
|
|
244
244
|
Every tool a server lists sits in the agent's context for the entire session, so this server
|
|
245
245
|
offers one.
|
|
246
246
|
|
|
247
|
-
| Offered as
|
|
248
|
-
|
|
|
249
|
-
| The `hardfin` tool
|
|
250
|
-
| `hardfin://guide`
|
|
251
|
-
| `hardfin://commands` | The command tree as JSON
|
|
247
|
+
| Offered as | Holds | Costs context |
|
|
248
|
+
| -------------------- | ---------------------------------------------------------------- | --------------------------------- |
|
|
249
|
+
| The `hardfin` tool | A list of arguments, such as `["asset", "list", "--limit", "5"]` | Always, and it is about 400 bytes |
|
|
250
|
+
| `hardfin://guide` | Every command, its flags, and the exit codes | Only when the agent reads it |
|
|
251
|
+
| `hardfin://commands` | The command tree as JSON | Only when the agent reads it |
|
|
252
252
|
|
|
253
253
|
Fifty tools would describe the same surface and crowd out the work. An agent that needs the
|
|
254
254
|
list reads a resource, or runs `["--help"]`.
|
|
@@ -272,14 +272,14 @@ hardfin status --json # for a support request
|
|
|
272
272
|
hardfin status --offline # no network calls at all
|
|
273
273
|
```
|
|
274
274
|
|
|
275
|
-
| Section
|
|
276
|
-
|
|
|
277
|
-
| `cli`
|
|
278
|
-
| `host`
|
|
279
|
-
| `configuration`
|
|
280
|
-
| `credential`
|
|
281
|
-
| `authorizationServer` | Whether discovery worked, and every endpoint it named
|
|
282
|
-
| `api`
|
|
275
|
+
| Section | Holds |
|
|
276
|
+
| --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
277
|
+
| `cli` | The version, the API version it speaks, Node, and whether a terminal is attached |
|
|
278
|
+
| `host` | The operating system, kernel, build, processor, memory, timezone, shell, and the distribution and WSL details on Linux |
|
|
279
|
+
| `configuration` | Every setting, its value, and the layer it came from |
|
|
280
|
+
| `credential` | Whether you are signed in, where the refresh token is kept, and when it was issued and last renewed |
|
|
281
|
+
| `authorizationServer` | Whether discovery worked, and every endpoint it named |
|
|
282
|
+
| `api` | Whether the API accepted the credential, when the access token expires, and its scopes |
|
|
283
283
|
|
|
284
284
|
Nothing secret is printed. An API key and a refresh token are each reported as a
|
|
285
285
|
`sha256:` fingerprint, which identifies a credential across two machines without disclosing
|
|
@@ -294,13 +294,13 @@ says which of the two you are looking at.
|
|
|
294
294
|
A local build reaches a local server without editing code. Four layers supply the same
|
|
295
295
|
settings, and the one nearest the top wins.
|
|
296
296
|
|
|
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
|
|
297
|
+
| Layer | Where | Beats |
|
|
298
|
+
| ----------- | --------------------------------------------------------------------- | ---------------- |
|
|
299
|
+
| Flag | `--api-url` | everything below |
|
|
300
|
+
| Environment | an exported `HARDFIN_*` variable | the files below |
|
|
301
|
+
| Env file | `.env` in the working directory, or the file `HARDFIN_ENV_FILE` names | the config file |
|
|
302
|
+
| Config file | `config.local.json` in the working directory | the defaults |
|
|
303
|
+
| Default | the published API | nothing |
|
|
304
304
|
|
|
305
305
|
An exported variable beats `.env` because Node leaves a variable that is already set alone.
|
|
306
306
|
|
|
@@ -336,6 +336,24 @@ The CLI reads whatever `config.local.json` and `.env` sit in the directory you r
|
|
|
336
336
|
A directory you do not control can therefore point the CLI at a server you do not expect, so
|
|
337
337
|
run `hardfin config` when a command reaches somewhere surprising.
|
|
338
338
|
|
|
339
|
+
## Working on the CLI
|
|
340
|
+
|
|
341
|
+
```sh
|
|
342
|
+
npm run check-types # tsc
|
|
343
|
+
npm run lint # eslint, with type-aware rules
|
|
344
|
+
npm run prettier:write # formatting
|
|
345
|
+
npm test # vitest
|
|
346
|
+
npm run build # tsdown, into dist/cli.js
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
CI runs all five on every pull request, plus a check that the generated commands still match
|
|
350
|
+
the vendored API document. A pull request that fails any of them cannot merge.
|
|
351
|
+
|
|
352
|
+
Two lint rules exist because of defects that reached the repository. `no-floating-promises`
|
|
353
|
+
covers a promise nobody waits for, which once killed the process when a sign in was
|
|
354
|
+
cancelled. `no-base-to-string` covers a value stringified as `[object Object]`, which a
|
|
355
|
+
request would have carried to the API.
|
|
356
|
+
|
|
339
357
|
## Releasing
|
|
340
358
|
|
|
341
359
|
This section is for anyone who merges a pull request in this repository. It tells you where
|
|
@@ -358,11 +376,11 @@ package published from a public repository, and this repository is private.
|
|
|
358
376
|
|
|
359
377
|
### What each merge publishes
|
|
360
378
|
|
|
361
|
-
| Merge
|
|
362
|
-
|
|
|
363
|
-
| Pull request into `dev` | always
|
|
364
|
-
| Promotion into `main`
|
|
365
|
-
| Hotfix into `main`
|
|
379
|
+
| Merge | Publishes | Version | Dist-tag |
|
|
380
|
+
| ----------------------- | ---------------------------------------------------------- | ---------------------------------------------- | -------- |
|
|
381
|
+
| Pull request into `dev` | always | the next version, suffixed `-dev.<run number>` | `dev` |
|
|
382
|
+
| Promotion into `main` | when `package.json` names a version that is not yet on npm | that version | `latest` |
|
|
383
|
+
| Hotfix into `main` | same rule as a promotion | that version | `latest` |
|
|
366
384
|
|
|
367
385
|
A preview never moves `latest`, so `npm install @hardfin/cli` keeps returning the released
|
|
368
386
|
version.
|
|
@@ -395,13 +413,13 @@ on `main` until someone brings it back.
|
|
|
395
413
|
A required check reads `version` from `package.json` on every pull request and judges it
|
|
396
414
|
against the branch the pull request targets.
|
|
397
415
|
|
|
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
|
|
416
|
+
| Version | Into `dev` | Into `main` |
|
|
417
|
+
| ------------------------------------------------ | ---------- | -------------------------------------- |
|
|
418
|
+
| Plain, above what `main` holds | passes | passes |
|
|
419
|
+
| Plain, equal to what `main` holds | passes | refused, because npm already serves it |
|
|
420
|
+
| Plain, below what `main` holds | refused | refused |
|
|
421
|
+
| Carrying a `-dev` or any other prerelease suffix | refused | refused |
|
|
422
|
+
| Not a semver version | refused | refused |
|
|
405
423
|
|
|
406
424
|
A prerelease suffix is refused everywhere because the release workflow appends it at publish
|
|
407
425
|
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") {
|
|
@@ -372,7 +382,7 @@ async function toTokens(url, form) {
|
|
|
372
382
|
body: new URLSearchParams(form)
|
|
373
383
|
});
|
|
374
384
|
const body = await response.json().catch(() => void 0);
|
|
375
|
-
if (!response.ok) throw new GrantFailure(
|
|
385
|
+
if (!response.ok) throw new GrantFailure(toText(body?.["error"] ?? `http_${response.status}`), body?.["error_description"] === void 0 ? void 0 : toText(body["error_description"]));
|
|
376
386
|
const parsed = TokenResponse.safeParse(body);
|
|
377
387
|
if (!parsed.success) throw new GrantFailure("invalid_response", "the token endpoint did not answer with a token");
|
|
378
388
|
return {
|
|
@@ -477,12 +487,12 @@ function forgetKeyring(issuer) {
|
|
|
477
487
|
function forgetFile(issuer) {
|
|
478
488
|
const held = readFile();
|
|
479
489
|
if (held[issuer] === void 0) return;
|
|
480
|
-
|
|
481
|
-
if (Object.keys(
|
|
490
|
+
const remaining = Object.fromEntries(Object.entries(held).filter(([name]) => name !== issuer));
|
|
491
|
+
if (Object.keys(remaining).length === 0) {
|
|
482
492
|
rmSync(toCredentialPath(), { force: true });
|
|
483
493
|
return;
|
|
484
494
|
}
|
|
485
|
-
writeFile(
|
|
495
|
+
writeFile(remaining);
|
|
486
496
|
}
|
|
487
497
|
/** toKeyring opens the OS keyring, or answers undefined where the platform has none. */
|
|
488
498
|
function toKeyring(issuer) {
|
|
@@ -834,7 +844,7 @@ async function runApi(input) {
|
|
|
834
844
|
writeData((await request({
|
|
835
845
|
apiUrl: input.resolved.settings.apiUrl,
|
|
836
846
|
credential: await toRequestCredential(input.resolved.settings),
|
|
837
|
-
method:
|
|
847
|
+
method: toText(input.flags["method"] ?? "GET").toUpperCase(),
|
|
838
848
|
path,
|
|
839
849
|
query,
|
|
840
850
|
body
|
|
@@ -962,7 +972,7 @@ function toFlagNames(command) {
|
|
|
962
972
|
return [...(command?.flags ?? []).map((flag) => `--${flag.name}`), "--help"];
|
|
963
973
|
}
|
|
964
974
|
async function runCompletion(input) {
|
|
965
|
-
const shell =
|
|
975
|
+
const shell = input.args[0] ?? "";
|
|
966
976
|
if (!SHELLS.includes(shell)) {
|
|
967
977
|
writeFailure(`${shell || "no shell"} is not one this CLI writes for. Choose ${SHELLS.join(", ")}`, input.isJSON);
|
|
968
978
|
return ExitCode.USAGE;
|
|
@@ -1116,7 +1126,7 @@ async function toDeviceAuthorization(endpoint, clientId, scope) {
|
|
|
1116
1126
|
})
|
|
1117
1127
|
});
|
|
1118
1128
|
const body = await response.json().catch(() => void 0);
|
|
1119
|
-
if (!response.ok) throw new GrantFailure(
|
|
1129
|
+
if (!response.ok) throw new GrantFailure(toText(body?.["error"] ?? `http_${response.status}`), body?.["error_description"] === void 0 ? void 0 : toText(body["error_description"]));
|
|
1120
1130
|
const parsed = DeviceResponse.safeParse(body);
|
|
1121
1131
|
if (!parsed.success) throw new GrantFailure("invalid_response", "the device endpoint did not answer with a code");
|
|
1122
1132
|
return {
|
|
@@ -1192,8 +1202,12 @@ async function toListener(timeoutMs) {
|
|
|
1192
1202
|
}, timeoutMs);
|
|
1193
1203
|
return {
|
|
1194
1204
|
redirectUri: `http://${HOST}:${server.address().port}${CALLBACK_PATH}`,
|
|
1195
|
-
callback: callback.finally(() =>
|
|
1196
|
-
|
|
1205
|
+
callback: callback.finally(() => {
|
|
1206
|
+
close(server, timer);
|
|
1207
|
+
}),
|
|
1208
|
+
close: () => {
|
|
1209
|
+
close(server, timer);
|
|
1210
|
+
}
|
|
1197
1211
|
};
|
|
1198
1212
|
}
|
|
1199
1213
|
function toCallback(request) {
|
|
@@ -1395,7 +1409,7 @@ async function runLogin(input) {
|
|
|
1395
1409
|
try {
|
|
1396
1410
|
const clientId = settings.clientId;
|
|
1397
1411
|
const metadata = await toMetadata(settings.issuerUrl);
|
|
1398
|
-
const scope =
|
|
1412
|
+
const scope = toText(input.flags["scope"] ?? DEFAULT_SCOPES);
|
|
1399
1413
|
if (input.flags["device"] === true) return await runDeviceLogin(input, metadata, scope);
|
|
1400
1414
|
const listener = await toListener(WAIT_MS);
|
|
1401
1415
|
const pkce = toPkce();
|
|
@@ -1551,7 +1565,9 @@ async function runLogout(input) {
|
|
|
1551
1565
|
} catch {
|
|
1552
1566
|
revoked = false;
|
|
1553
1567
|
}
|
|
1554
|
-
await withLock(() =>
|
|
1568
|
+
await withLock(() => {
|
|
1569
|
+
forget(settings.issuerUrl);
|
|
1570
|
+
});
|
|
1555
1571
|
forgetHeldTokens();
|
|
1556
1572
|
writeData({
|
|
1557
1573
|
signedOut: true,
|
|
@@ -1613,7 +1629,7 @@ async function toResponse(request, commands, run) {
|
|
|
1613
1629
|
description: "The command tree as JSON",
|
|
1614
1630
|
mimeType: "application/json"
|
|
1615
1631
|
}] });
|
|
1616
|
-
case "resources/read": return answer(toResource(
|
|
1632
|
+
case "resources/read": return answer(toResource(toText(request.params?.["uri"] ?? ""), commands));
|
|
1617
1633
|
case "tools/call": return answer(await toToolResult(request.params, run));
|
|
1618
1634
|
case "ping": return answer({});
|
|
1619
1635
|
default:
|
|
@@ -1691,11 +1707,13 @@ function toCliRunner() {
|
|
|
1691
1707
|
child.stderr.on("data", (chunk) => {
|
|
1692
1708
|
stderr += chunk.toString();
|
|
1693
1709
|
});
|
|
1694
|
-
child.on("close", (code) =>
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1710
|
+
child.on("close", (code) => {
|
|
1711
|
+
resolve({
|
|
1712
|
+
stdout,
|
|
1713
|
+
stderr,
|
|
1714
|
+
code: code ?? 1
|
|
1715
|
+
});
|
|
1716
|
+
});
|
|
1699
1717
|
});
|
|
1700
1718
|
}
|
|
1701
1719
|
/** serve answers requests on stdin until the client closes it. */
|
|
@@ -1861,7 +1879,7 @@ function toQuery(operation, flags) {
|
|
|
1861
1879
|
if (value === void 0) continue;
|
|
1862
1880
|
const parsed = flag.schema.safeParse(value);
|
|
1863
1881
|
const carried = parsed.success ? parsed.data : value;
|
|
1864
|
-
for (const entry of Array.isArray(carried) ? carried : [carried]) query.append(flag.queryName,
|
|
1882
|
+
for (const entry of Array.isArray(carried) ? carried : [carried]) query.append(flag.queryName, toText(entry));
|
|
1865
1883
|
}
|
|
1866
1884
|
return query;
|
|
1867
1885
|
}
|
|
@@ -1883,7 +1901,7 @@ async function toForm(upload, flags) {
|
|
|
1883
1901
|
const value = flags[toOptionKey(field.name)];
|
|
1884
1902
|
if (value !== void 0) {
|
|
1885
1903
|
const parsed = field.schema.safeParse(value);
|
|
1886
|
-
form.append(field.jsonPath[0] ?? field.name,
|
|
1904
|
+
form.append(field.jsonPath[0] ?? field.name, toText(parsed.success ? parsed.data : value));
|
|
1887
1905
|
}
|
|
1888
1906
|
}
|
|
1889
1907
|
return form;
|
|
@@ -1896,7 +1914,7 @@ function toBody(operation, flags) {
|
|
|
1896
1914
|
const value = flags[toOptionKey(flag.name)];
|
|
1897
1915
|
if (value === void 0) continue;
|
|
1898
1916
|
if (flag.element) {
|
|
1899
|
-
const elements = toElements(flag, Array.isArray(value) ? value.map(
|
|
1917
|
+
const elements = toElements(flag, Array.isArray(value) ? value.map(toText) : [toText(value)]);
|
|
1900
1918
|
if (elements instanceof Error) return elements;
|
|
1901
1919
|
set(body, flag.jsonPath, elements);
|
|
1902
1920
|
hasField = true;
|
|
@@ -1913,7 +1931,7 @@ function toBody(operation, flags) {
|
|
|
1913
1931
|
/** toCleared sends null for each field named by --unset, which is how a field is cleared. */
|
|
1914
1932
|
function toCleared(operation, flags, body) {
|
|
1915
1933
|
const named = flags["unset"];
|
|
1916
|
-
const names = Array.isArray(named) ? named.map(
|
|
1934
|
+
const names = Array.isArray(named) ? named.map(toText) : named === void 0 ? [] : [toText(named)];
|
|
1917
1935
|
for (const name of names) {
|
|
1918
1936
|
const flag = operation.bodyFlags.find((entry) => entry.name === name.replace(/^--/, ""));
|
|
1919
1937
|
if (!flag) return /* @__PURE__ */ new Error(`--unset names no field called ${name}`);
|
|
@@ -4694,27 +4712,24 @@ function toEnvironmentReport() {
|
|
|
4694
4712
|
apiVersion: API_VERSION,
|
|
4695
4713
|
node: process.version,
|
|
4696
4714
|
platform: process.platform,
|
|
4697
|
-
interactive:
|
|
4715
|
+
interactive: process.stdin.isTTY
|
|
4698
4716
|
};
|
|
4699
4717
|
}
|
|
4700
4718
|
/** toConfigurationReport names every setting, its source, and never a secret's value. */
|
|
4701
4719
|
function toConfigurationReport(resolved) {
|
|
4702
|
-
const
|
|
4720
|
+
const report = { configFile: CONFIG_FILE };
|
|
4721
|
+
for (const [key, value] of Object.entries(resolved.settings)) {
|
|
4703
4722
|
const from = resolved.sources[key];
|
|
4704
|
-
|
|
4723
|
+
report[key] = key === "apiKey" ? {
|
|
4705
4724
|
set: value !== void 0,
|
|
4706
4725
|
fingerprint: toFingerprint(value),
|
|
4707
4726
|
from
|
|
4708
|
-
}
|
|
4709
|
-
return [key, {
|
|
4727
|
+
} : {
|
|
4710
4728
|
value: value ?? null,
|
|
4711
4729
|
from
|
|
4712
|
-
}
|
|
4713
|
-
}
|
|
4714
|
-
return
|
|
4715
|
-
...Object.fromEntries(entries),
|
|
4716
|
-
configFile: CONFIG_FILE
|
|
4717
|
-
};
|
|
4730
|
+
};
|
|
4731
|
+
}
|
|
4732
|
+
return report;
|
|
4718
4733
|
}
|
|
4719
4734
|
function toCredentialReport(apiKey, stored) {
|
|
4720
4735
|
if (apiKey) return {
|
|
@@ -4810,8 +4825,7 @@ function toFlattened(value) {
|
|
|
4810
4825
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return;
|
|
4811
4826
|
const holder = value;
|
|
4812
4827
|
if (holder.from === void 0) return;
|
|
4813
|
-
|
|
4814
|
-
return `${String(shown)} (${holder.from})`;
|
|
4828
|
+
return `${toText(holder.value ?? (holder.set ? holder.fingerprint ?? "set" : "not set"))} (${holder.from})`;
|
|
4815
4829
|
}
|
|
4816
4830
|
/** toLines lays the report out for a person, one indented line per value. */
|
|
4817
4831
|
function toLines(report, depth = 0) {
|
|
@@ -4913,7 +4927,7 @@ async function toExitCode(command, args, flags) {
|
|
|
4913
4927
|
}
|
|
4914
4928
|
function toArgumentList(value) {
|
|
4915
4929
|
if (Array.isArray(value)) return value.map(String);
|
|
4916
|
-
return value === void 0 ? [] : [
|
|
4930
|
+
return value === void 0 ? [] : [toText(value)];
|
|
4917
4931
|
}
|
|
4918
4932
|
function collect(value, previous) {
|
|
4919
4933
|
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.16",
|
|
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"
|