@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.
Files changed (3) hide show
  1. package/README.md +84 -66
  2. package/dist/cli.js +49 -35
  3. 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 | 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 |
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 | 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>` |
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 | 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 |
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 | Refuses |
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 | a vendored document that was updated without regenerating the commands |
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 | Does |
155
- | --- | --- |
156
- | `c` | Copies the URL to the clipboard |
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 | Kept in |
194
- | --- | --- |
195
- | macOS, Windows, and Linux with a secret service | the OS keyring |
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 | 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 |
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 | 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 |
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 | 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 |
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 | 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 |
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 | Publishes | Version | Dist-tag |
362
- | --- | --- | --- | --- |
363
- | Pull request into `dev` | always | the next version, suffixed `-dev.<run number>` | `dev` |
364
- | Promotion into `main` | when `package.json` names a version that is not yet on npm | that version | `latest` |
365
- | Hotfix into `main` | same rule as a promotion | that version | `latest` |
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 | Into `dev` | Into `main` |
399
- | --- | --- | --- |
400
- | Plain, above what `main` holds | passes | passes |
401
- | Plain, equal to what `main` holds | passes | refused, because npm already serves it |
402
- | Plain, below what `main` holds | refused | refused |
403
- | Carrying a `-dev` or any other prerelease suffix | refused | refused |
404
- | Not a semver version | refused | refused |
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(String(body?.["error"] ?? `http_${response.status}`), body?.["error_description"] === void 0 ? void 0 : String(body["error_description"]));
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
- delete held[issuer];
481
- if (Object.keys(held).length === 0) {
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(held);
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: String(input.flags["method"] ?? "GET").toUpperCase(),
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 = String(input.args[0] ?? "");
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(String(body?.["error"] ?? `http_${response.status}`), body?.["error_description"] === void 0 ? void 0 : String(body["error_description"]));
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(() => close(server, timer)),
1196
- close: () => close(server, timer)
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 = String(input.flags["scope"] ?? DEFAULT_SCOPES);
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(() => forget(settings.issuerUrl));
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(String(request.params?.["uri"] ?? ""), commands));
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) => resolve({
1695
- stdout,
1696
- stderr,
1697
- code: code ?? 1
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, String(entry));
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, String(parsed.success ? parsed.data : value));
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(String) : [String(value)]);
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(String) : named === void 0 ? [] : [String(named)];
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: Boolean(process.stdin.isTTY)
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 entries = Object.entries(resolved.settings).map(([key, value]) => {
4720
+ const report = { configFile: CONFIG_FILE };
4721
+ for (const [key, value] of Object.entries(resolved.settings)) {
4703
4722
  const from = resolved.sources[key];
4704
- if (key === "apiKey") return [key, {
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
- const shown = holder.value ?? (holder.set ? holder.fingerprint ?? "set" : "not set");
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 ? [] : [String(value)];
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.15",
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"