@hardfin/cli 0.0.2-dev.9 → 0.1.0-dev.20

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 +191 -62
  2. package/dist/cli.js +2713 -363
  3. package/package.json +14 -2
package/README.md CHANGED
@@ -26,30 +26,66 @@ 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
- A path parameter becomes a positional argument. A query parameter becomes a flag, named in
51
- kebab case, and an array parameter becomes a flag you repeat. An enum parameter carries its
52
- values, so a wrong value fails locally with exit code 2 rather than at the API.
50
+ Everything an endpoint takes becomes a flag or an argument. No command asks for JSON.
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 |
60
+
61
+ ```sh
62
+ hardfin asset ownership create ast_4f9xk2mq7plr8stz \
63
+ --customer-id cst_a1 --date 2026-09-23 --sale-price 1500.00
64
+
65
+ hardfin asset move execute create \
66
+ --move assetId=0192f7d4-3b6a-7c1e-9f2d-5a8b4c6e7d70,originId=0192f7d4-3b6a-7c1e-9f2d-5a8b4c6e7d71,destinationId=0192f7d4-3b6a-7c1e-9f2d-5a8b4c6e7d72,shipAt=2026-09-24T10:00:00Z
67
+ ```
68
+
69
+ A value is sent as the type the document names. `--useful-life 36` sends the number 36. A
70
+ decimal such as `--salvage-value 1500.00` stays a string, which is how the API takes money
71
+ without losing precision.
72
+
73
+ A download is written where you name it, and a terminal is never filled with a file's bytes.
74
+
75
+ ```sh
76
+ hardfin file get file_7hq2mx9pkr4stz8w --output photo.jpg
77
+ hardfin file get file_7hq2mx9pkr4stz8w --output - | wc -c
78
+ ```
79
+
80
+ Clear a field with `--unset`, naming the flag:
81
+
82
+ ```sh
83
+ hardfin asset accounting update ast_4f9xk2mq7plr8stz --unset useful-life
84
+ ```
85
+
86
+ Three things fail locally with exit code 2, before any request is made: a value outside an
87
+ enum, a required field that is missing, and `--unset` on a field the API will not take null
88
+ for.
53
89
 
54
90
  ### Overrides
55
91
 
@@ -88,10 +124,10 @@ what the api-spec bridge publishes. Commit the document and the generated comman
88
124
 
89
125
  Two checks keep the pair honest.
90
126
 
91
- | Check | Refuses |
92
- | --- | --- |
127
+ | Check | Refuses |
128
+ | ------------- | ----------------------------------------------------------------------------- |
93
129
  | The generator | a document whose `info.version` is not a date, which means an earlier release |
94
- | CI | a vendored document that was updated without regenerating the commands |
130
+ | CI | a vendored document that was updated without regenerating the commands |
95
131
 
96
132
  The first one matters because `hardfinhq/api-spec` can sit a release behind the monorepo
97
133
  while its bridge pull request is open. Generating from that document would replace the
@@ -100,9 +136,10 @@ current commands with an earlier API's.
100
136
  ## Signing in
101
137
 
102
138
  ```sh
103
- hardfin login # approve this CLI in a browser
104
- hardfin status # what the CLI is configured with and signed in as
105
- hardfin logout # forget this machine, and ask Hardfin to revoke it
139
+ hardfin login # approve this CLI in a browser
140
+ hardfin login --device # approve on another machine, by typing a code
141
+ hardfin status # what the CLI is configured with and signed in as
142
+ hardfin logout # forget this machine, and ask Hardfin to revoke it
106
143
  ```
107
144
 
108
145
  An API key is still the way to run unattended. `HARDFIN_API_KEY` wins over a stored sign in,
@@ -120,9 +157,9 @@ so CI and an agent sandbox need no browser.
120
157
 
121
158
  While it waits, the terminal takes two things.
122
159
 
123
- | Key | Does |
124
- | --- | --- |
125
- | `c` | Copies the URL to the clipboard |
160
+ | Key | Does |
161
+ | -------------------------- | ----------------------------------------- |
162
+ | `c` | Copies the URL to the clipboard |
126
163
  | A pasted value, then enter | Finishes the sign in without the listener |
127
164
 
128
165
  Paste whichever of these you have: the whole redirect URL from the browser's address bar,
@@ -135,11 +172,33 @@ On WSL the browser is opened through Windows, by `wslview`, then PowerShell, the
135
172
  The port changes every time, so Hardfin matches a loopback redirect by everything except its
136
173
  port, which is what RFC 8252 asks of an authorization server.
137
174
 
175
+ ### Signing in from a machine with no browser
176
+
177
+ `hardfin login --device` prints a short code and a page to enter it on, and waits. Approve
178
+ on your phone or any other machine, and the terminal finishes on its own.
179
+
180
+ ```
181
+ Code WDJB-MJHT
182
+ At https://app.hardfin.com/device
183
+ ```
184
+
185
+ The CLI refuses `--device` when the authorization server does not offer that grant, rather
186
+ than waiting for an endpoint that is not there.
187
+
188
+ ### When a sign in stops working
189
+
190
+ A refresh token can die: someone signed out on another machine, the token was presented
191
+ twice, or it aged out. The server answers `invalid_grant`, and the CLI removes the dead
192
+ credential and tells you to run `hardfin login` again.
193
+
194
+ A server that merely cannot be reached does not count, because a failed connection says
195
+ nothing about whether the credential is good.
196
+
138
197
  ### Where the refresh token is kept
139
198
 
140
- | Host | Kept in |
141
- | --- | --- |
142
- | macOS, Windows, and Linux with a secret service | the OS keyring |
199
+ | Host | Kept in |
200
+ | --------------------------------------------------------------- | ----------------------------------------------------- |
201
+ | macOS, Windows, and Linux with a secret service | the OS keyring |
143
202
  | Everything else, including WSL, containers, and agent sandboxes | `$XDG_STATE_HOME/hardfin/credentials.json`, mode 0600 |
144
203
 
145
204
  `HARDFIN_NO_BROWSER=1` keeps `hardfin login` from opening a tab, which is what a script or a
@@ -157,10 +216,57 @@ production never share a sign in.
157
216
  Nothing. Hardfin registers this CLI as a first-party client, and its key is the same in every
158
217
  environment, so `hardfin login` works out of the box.
159
218
 
160
- | Setting | Holds | Default |
161
- | --- | --- | --- |
162
- | `clientId`, or `HARDFIN_CLIENT_ID` | the client this CLI names itself as | the seeded Hardfin CLI client |
163
- | `issuerUrl`, or `HARDFIN_ISSUER_URL` | the authorization server | the API's host root |
219
+ | Setting | Holds | Default |
220
+ | ------------------------------------ | ----------------------------------- | ----------------------------- |
221
+ | `clientId`, or `HARDFIN_CLIENT_ID` | the client this CLI names itself as | the seeded Hardfin CLI client |
222
+ | `issuerUrl`, or `HARDFIN_ISSUER_URL` | the authorization server | the API's host root |
223
+
224
+ ## Completing commands in your shell
225
+
226
+ ```sh
227
+ source <(hardfin completion zsh) # this shell, now
228
+ hardfin completion zsh >> ~/.zshrc # every shell after this one
229
+ ```
230
+
231
+ `bash`, `zsh`, `fish` and `powershell` each get a script. The script asks the CLI what may
232
+ follow what you have typed, so completions never fall behind the commands, including the ones
233
+ generated from the API document.
234
+
235
+ ## Serving an agent
236
+
237
+ ```sh
238
+ hardfin mcp
239
+ ```
240
+
241
+ It speaks the Model Context Protocol on standard input and output, so Claude, Codex or any
242
+ other client can run Hardfin commands.
243
+
244
+ ```sh
245
+ claude mcp add hardfin -- hardfin mcp
246
+ ```
247
+
248
+ ### One tool, and resources for the rest
249
+
250
+ Every tool a server lists sits in the agent's context for the entire session, so this server
251
+ offers one.
252
+
253
+ | Offered as | Holds | Costs context |
254
+ | -------------------- | ---------------------------------------------------------------- | --------------------------------- |
255
+ | The `hardfin` tool | A list of arguments, such as `["asset", "list", "--limit", "5"]` | Always, and it is about 400 bytes |
256
+ | `hardfin://guide` | Every command, its flags, and the exit codes | Only when the agent reads it |
257
+ | `hardfin://commands` | The command tree as JSON | Only when the agent reads it |
258
+
259
+ Fifty tools would describe the same surface and crowd out the work. An agent that needs the
260
+ list reads a resource, or runs `["--help"]`.
261
+
262
+ A tool call runs the CLI itself, so an agent and a person get identical parsing, output and
263
+ exit codes.
264
+
265
+ ### What it signs in as
266
+
267
+ The server uses whatever credential this machine holds, so an agent acts as the person who
268
+ ran `hardfin login`. Give an agent an API key through `HARDFIN_API_KEY` when it should act
269
+ as an integration instead.
164
270
 
165
271
  ## Diagnosing a problem
166
272
 
@@ -172,35 +278,35 @@ hardfin status --json # for a support request
172
278
  hardfin status --offline # no network calls at all
173
279
  ```
174
280
 
175
- | Section | Holds |
176
- | --- | --- |
177
- | `cli` | The version, the API version it speaks, Node, and whether a terminal is attached |
178
- | `host` | The operating system, kernel, build, processor, memory, timezone, shell, and the distribution and WSL details on Linux |
179
- | `configuration` | Every setting, its value, and the layer it came from |
180
- | `credential` | Whether you are signed in, where the refresh token is kept, and when it was issued and last renewed |
181
- | `authorizationServer` | Whether discovery worked, and every endpoint it named |
182
- | `api` | Whether the API accepted the credential, when the access token expires, and its scopes |
281
+ | Section | Holds |
282
+ | --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
283
+ | `cli` | The version, the API version it speaks, Node, and whether a terminal is attached |
284
+ | `host` | The operating system, kernel, build, processor, memory, timezone, shell, and the distribution and WSL details on Linux |
285
+ | `configuration` | Every setting, its value, and the layer it came from |
286
+ | `credential` | Whether you are signed in, where the refresh token is kept, and when it was issued and last renewed |
287
+ | `authorizationServer` | Whether discovery worked, and every endpoint it named |
288
+ | `api` | Whether the API accepted the credential, when the access token expires, and its scopes |
183
289
 
184
290
  Nothing secret is printed. An API key and a refresh token are each reported as a
185
291
  `sha256:` fingerprint, which identifies a credential across two machines without disclosing
186
292
  it.
187
293
 
188
- `refreshExpiresAt` is an estimate. Hardfin expires an unused refresh token after 90 days,
189
- but the token endpoint reports no expiry, so the CLI applies that rule to the date it last
190
- renewed. The flag `refreshExpiryIsEstimated` says so in the output.
294
+ `refreshExpiresAt` is what the server said when it issued the token. A sign in stored before
295
+ the server reported one falls back to Hardfin's 90-day rule, and `refreshExpiryIsEstimated`
296
+ says which of the two you are looking at.
191
297
 
192
298
  ## Local configuration
193
299
 
194
300
  A local build reaches a local server without editing code. Four layers supply the same
195
301
  settings, and the one nearest the top wins.
196
302
 
197
- | Layer | Where | Beats |
198
- | --- | --- | --- |
199
- | Flag | `--api-url` | everything below |
200
- | Environment | an exported `HARDFIN_*` variable | the files below |
201
- | Env file | `.env` in the working directory, or the file `HARDFIN_ENV_FILE` names | the config file |
202
- | Config file | `config.local.json` in the working directory | the defaults |
203
- | Default | the published API | nothing |
303
+ | Layer | Where | Beats |
304
+ | ----------- | --------------------------------------------------------------------- | ---------------- |
305
+ | Flag | `--api-url` | everything below |
306
+ | Environment | an exported `HARDFIN_*` variable | the files below |
307
+ | Env file | `.env` in the working directory, or the file `HARDFIN_ENV_FILE` names | the config file |
308
+ | Config file | `config.local.json` in the working directory | the defaults |
309
+ | Default | the published API | nothing |
204
310
 
205
311
  An exported variable beats `.env` because Node leaves a variable that is already set alone.
206
312
 
@@ -236,6 +342,29 @@ The CLI reads whatever `config.local.json` and `.env` sit in the directory you r
236
342
  A directory you do not control can therefore point the CLI at a server you do not expect, so
237
343
  run `hardfin config` when a command reaches somewhere surprising.
238
344
 
345
+ A sign in is not carried there. An access token is sent only to the host that issued it, and
346
+ only over https unless that host is this machine. Pointing `apiUrl` elsewhere gets a refusal
347
+ naming both hosts, rather than a token sent to a stranger. An API key is sent wherever you
348
+ point it, because setting one is a deliberate act.
349
+
350
+ ## Working on the CLI
351
+
352
+ ```sh
353
+ npm run check-types # tsc
354
+ npm run lint # eslint, with type-aware rules
355
+ npm run prettier:write # formatting
356
+ npm test # vitest
357
+ npm run build # tsdown, into dist/cli.js
358
+ ```
359
+
360
+ CI runs all five on every pull request, plus a check that the generated commands still match
361
+ the vendored API document. A pull request that fails any of them cannot merge.
362
+
363
+ Two lint rules exist because of defects that reached the repository. `no-floating-promises`
364
+ covers a promise nobody waits for, which once killed the process when a sign in was
365
+ cancelled. `no-base-to-string` covers a value stringified as `[object Object]`, which a
366
+ request would have carried to the API.
367
+
239
368
  ## Releasing
240
369
 
241
370
  This section is for anyone who merges a pull request in this repository. It tells you where
@@ -258,11 +387,11 @@ package published from a public repository, and this repository is private.
258
387
 
259
388
  ### What each merge publishes
260
389
 
261
- | Merge | Publishes | Version | Dist-tag |
262
- | --- | --- | --- | --- |
263
- | Pull request into `dev` | always | the next version, suffixed `-dev.<run number>` | `dev` |
264
- | Promotion into `main` | when `package.json` names a version that is not yet on npm | that version | `latest` |
265
- | Hotfix into `main` | same rule as a promotion | that version | `latest` |
390
+ | Merge | Publishes | Version | Dist-tag |
391
+ | ----------------------- | ---------------------------------------------------------- | ---------------------------------------------- | -------- |
392
+ | Pull request into `dev` | always | the next version, suffixed `-dev.<run number>` | `dev` |
393
+ | Promotion into `main` | when `package.json` names a version that is not yet on npm | that version | `latest` |
394
+ | Hotfix into `main` | same rule as a promotion | that version | `latest` |
266
395
 
267
396
  A preview never moves `latest`, so `npm install @hardfin/cli` keeps returning the released
268
397
  version.
@@ -295,13 +424,13 @@ on `main` until someone brings it back.
295
424
  A required check reads `version` from `package.json` on every pull request and judges it
296
425
  against the branch the pull request targets.
297
426
 
298
- | Version | Into `dev` | Into `main` |
299
- | --- | --- | --- |
300
- | Plain, above what `main` holds | passes | passes |
301
- | Plain, equal to what `main` holds | passes | refused, because npm already serves it |
302
- | Plain, below what `main` holds | refused | refused |
303
- | Carrying a `-dev` or any other prerelease suffix | refused | refused |
304
- | Not a semver version | refused | refused |
427
+ | Version | Into `dev` | Into `main` |
428
+ | ------------------------------------------------ | ---------- | -------------------------------------- |
429
+ | Plain, above what `main` holds | passes | passes |
430
+ | Plain, equal to what `main` holds | passes | refused, because npm already serves it |
431
+ | Plain, below what `main` holds | refused | refused |
432
+ | Carrying a `-dev` or any other prerelease suffix | refused | refused |
433
+ | Not a semver version | refused | refused |
305
434
 
306
435
  A prerelease suffix is refused everywhere because the release workflow appends it at publish
307
436
  time. A version equal to `main`'s is fine on `dev`, since the workflow increments the patch