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

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 +187 -62
  2. package/dist/cli.js +2498 -317
  3. package/package.json +14 -2
package/dist/cli.js CHANGED
@@ -2,12 +2,13 @@
2
2
  import { createRequire } from "node:module";
3
3
  import { Command, Option } from "commander";
4
4
  import { z } from "zod";
5
- import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
6
- import { dirname, join, resolve } from "node:path";
5
+ import { chmodSync, closeSync, existsSync, mkdirSync, openAsBlob, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
6
+ import { basename, dirname, join, resolve } from "node:path";
7
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
7
8
  import { arch, cpus, homedir, release, totalmem, type, version } from "node:os";
8
- import { spawnSync } from "node:child_process";
9
+ import { spawn, spawnSync } from "node:child_process";
9
10
  import { createServer } from "node:http";
10
- import { createHash, randomBytes } from "node:crypto";
11
+ import { createInterface } from "node:readline";
11
12
  //#region src/command/registry.ts
12
13
  /** ExitCode is what the process returns, and what an agent branches on. */
13
14
  const ExitCode = {
@@ -119,13 +120,23 @@ function toOrigin(apiUrl) {
119
120
  }
120
121
  //#endregion
121
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
+ }
122
133
  /** writeData prints a command's result on stdout. */
123
134
  function writeData(value) {
124
135
  if (typeof value === "string") {
125
136
  process.stdout.write(value.endsWith("\n") ? value : `${value}\n`);
126
137
  return;
127
138
  }
128
- process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
139
+ process.stdout.write(`${JSON.stringify(value ?? null, null, 2)}\n`);
129
140
  }
130
141
  /** writeFailure prints why a command failed on stderr, as text or as JSON. */
131
142
  function writeFailure(message, isJSON, errors, requestId) {
@@ -197,24 +208,34 @@ function toGuide(commands, version) {
197
208
  "## Commands",
198
209
  ""
199
210
  ];
200
- for (const command of commands) {
201
- lines.push(`### \`hardfin ${command.name}\``, "", command.description ?? command.summary, "");
202
- if (command.arguments.length > 0) {
203
- lines.push("| Argument | Required | Holds |", "| --- | --- | --- |");
204
- for (const argument of command.arguments) lines.push(`| \`${argument.name}\` | ${argument.required ? "yes" : "no"} | ${argument.description} |`);
205
- lines.push("");
206
- }
207
- if (command.flags.length > 0) {
208
- lines.push("| Flag | Takes | Does |", "| --- | --- | --- |");
209
- for (const flag of command.flags) {
210
- const name = flag.short ? `-${flag.short}, --${flag.name}` : `--${flag.name}`;
211
- lines.push(`| \`${name}\` | ${flag.valueName ?? "nothing"} | ${flag.description} |`);
212
- }
213
- lines.push("");
211
+ for (const command of commands.filter((command) => !command.hidden)) lines.push(...toCommandLines(command, []));
212
+ return lines.join("\n");
213
+ }
214
+ /** toCommandLines describes one command and everything nested under it. */
215
+ function toCommandLines(command, parents) {
216
+ const path = [...parents, command.name];
217
+ const lines = [
218
+ `### \`hardfin ${path.join(" ")}\``,
219
+ "",
220
+ command.description ?? command.summary,
221
+ ""
222
+ ];
223
+ if (command.arguments.length > 0) {
224
+ lines.push("| Argument | Required | Holds |", "| --- | --- | --- |");
225
+ for (const argument of command.arguments) lines.push(`| \`${argument.name}\` | ${argument.required ? "yes" : "no"} | ${argument.description} |`);
226
+ lines.push("");
227
+ }
228
+ if (command.flags.length > 0) {
229
+ lines.push("| Flag | Takes | Does |", "| --- | --- | --- |");
230
+ for (const flag of command.flags) {
231
+ const name = flag.short ? `-${flag.short}, --${flag.name}` : `--${flag.name}`;
232
+ lines.push(`| \`${name}\` | ${flag.valueName ?? "nothing"} | ${flag.description} |`);
214
233
  }
215
- for (const example of command.examples) lines.push(`${example.description}:`, "", "```sh", example.command, "```", "");
234
+ lines.push("");
216
235
  }
217
- return lines.join("\n");
236
+ for (const example of command.examples) lines.push(`${example.description}:`, "", "```sh", example.command, "```", "");
237
+ for (const subcommand of command.subcommands ?? []) lines.push(...toCommandLines(subcommand, path));
238
+ return lines;
218
239
  }
219
240
  async function runAgentGuide(input) {
220
241
  if (input.flags["json"] === true) {
@@ -237,10 +258,37 @@ function toSummary(command) {
237
258
  valueName: flag.valueName ?? null,
238
259
  repeatable: flag.repeatable ?? false
239
260
  })),
240
- examples: command.examples
261
+ examples: command.examples,
262
+ subcommands: command.subcommands?.filter((entry) => !entry.hidden).map(toSummary)
241
263
  };
242
264
  }
243
265
  //#endregion
266
+ //#region src/auth/jwt.ts
267
+ /**
268
+ * toClaims reads an access token's payload for display. Nothing here verifies the
269
+ * signature, because the API is what decides whether a token is good.
270
+ */
271
+ function toClaims(token) {
272
+ const payload = token.split(".")[1];
273
+ if (!payload) return {};
274
+ try {
275
+ const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
276
+ return {
277
+ expiresAt: typeof decoded["exp"] === "number" ? decoded["exp"] * 1e3 : void 0,
278
+ issuedAt: typeof decoded["iat"] === "number" ? decoded["iat"] * 1e3 : void 0,
279
+ scopes: toScopes(decoded),
280
+ subject: typeof decoded["sub"] === "string" ? decoded["sub"] : void 0
281
+ };
282
+ } catch {
283
+ return {};
284
+ }
285
+ }
286
+ /** toScopes reads scp, which is where Hardfin puts an access token's scopes. */
287
+ function toScopes(decoded) {
288
+ if (Array.isArray(decoded["scp"])) return decoded["scp"].map(String);
289
+ return typeof decoded["scope"] === "string" ? decoded["scope"].split(" ") : void 0;
290
+ }
291
+ //#endregion
244
292
  //#region src/auth/metadata.ts
245
293
  const METADATA_PATH = "/.well-known/oauth-authorization-server";
246
294
  const AuthorizationServerMetadata = z.looseObject({
@@ -282,6 +330,7 @@ const TokenResponse = z.looseObject({
282
330
  token_type: z.string(),
283
331
  expires_in: z.number().optional(),
284
332
  refresh_token: z.string().optional(),
333
+ refresh_token_expires_in: z.number().optional(),
285
334
  scope: z.string().optional()
286
335
  });
287
336
  /** GrantFailure is a token request the authorization server refused. */
@@ -295,7 +344,7 @@ var GrantFailure = class extends Error {
295
344
  };
296
345
  /** toTokensFromCode trades an authorization code for tokens. */
297
346
  async function toTokensFromCode(tokenUrl, clientId, code, redirectUri, pkce) {
298
- return await request$1(tokenUrl, {
347
+ return await toTokens(tokenUrl, {
299
348
  grant_type: "authorization_code",
300
349
  client_id: clientId,
301
350
  code,
@@ -305,7 +354,7 @@ async function toTokensFromCode(tokenUrl, clientId, code, redirectUri, pkce) {
305
354
  }
306
355
  /** toTokensFromRefresh trades a refresh token for a fresh pair. */
307
356
  async function toTokensFromRefresh(tokenUrl, clientId, refreshToken) {
308
- return await request$1(tokenUrl, {
357
+ return await toTokens(tokenUrl, {
309
358
  grant_type: "refresh_token",
310
359
  client_id: clientId,
311
360
  refresh_token: refreshToken
@@ -323,8 +372,8 @@ async function revoke(revocationUrl, clientId, token) {
323
372
  })
324
373
  });
325
374
  }
326
- /** The token endpoint answers a flat RFC 6749 body, not the API's envelope. */
327
- async function request$1(url, form) {
375
+ /** toTokens asks the token endpoint, which answers a flat RFC 6749 body, not the envelope. */
376
+ async function toTokens(url, form) {
328
377
  const response = await fetch(url, {
329
378
  method: "POST",
330
379
  headers: {
@@ -334,12 +383,13 @@ async function request$1(url, form) {
334
383
  body: new URLSearchParams(form)
335
384
  });
336
385
  const body = await response.json().catch(() => void 0);
337
- if (!response.ok) throw new GrantFailure(String(body?.["error"] ?? `http_${response.status}`), body?.["error_description"] === void 0 ? void 0 : String(body["error_description"]));
386
+ if (!response.ok) throw new GrantFailure(toText(body?.["error"] ?? `http_${response.status}`), body?.["error_description"] === void 0 ? void 0 : toText(body["error_description"]));
338
387
  const parsed = TokenResponse.safeParse(body);
339
388
  if (!parsed.success) throw new GrantFailure("invalid_response", "the token endpoint did not answer with a token");
340
389
  return {
341
390
  accessToken: parsed.data.access_token,
342
391
  refreshToken: parsed.data.refresh_token,
392
+ refreshExpiresAt: parsed.data.refresh_token_expires_in === void 0 ? void 0 : Date.now() + parsed.data.refresh_token_expires_in * 1e3,
343
393
  scope: parsed.data.scope,
344
394
  expiresAt: parsed.data.expires_in === void 0 ? void 0 : Date.now() + parsed.data.expires_in * 1e3
345
395
  };
@@ -364,40 +414,55 @@ function toCredentialPath() {
364
414
  * family lifetime runs from. Callers hold the credential lock, which is what makes a write
365
415
  * from another process merge rather than disappear.
366
416
  */
367
- function keep(issuer, refreshToken) {
417
+ function keep(issuer, refreshToken, expiresAt) {
368
418
  const now = (/* @__PURE__ */ new Date()).toISOString();
369
419
  const record = {
370
420
  refreshToken,
371
421
  signedInAt: toCredential(issuer)?.signedInAt ?? now,
372
- renewedAt: now
422
+ renewedAt: now,
423
+ expiresAt: expiresAt === void 0 ? void 0 : new Date(expiresAt).toISOString()
373
424
  };
374
425
  const keyring = toKeyring(issuer);
375
426
  if (keyring) try {
376
427
  keyring.setPassword(JSON.stringify(record));
428
+ forgetFile(issuer);
377
429
  return "keyring";
378
430
  } catch {}
379
431
  writeFile({
380
432
  ...readFile(),
381
433
  [issuer]: record
382
434
  });
435
+ forgetKeyring(issuer);
383
436
  return "file";
384
437
  }
385
- /** toCredential reads what is held for an issuer, and where it was held. */
438
+ /**
439
+ * toCredential reads what is held for an issuer, and where it was held. Both backends are
440
+ * read, because a host that lost its keyring for a while wrote to the file instead, and the
441
+ * newer of the two is the one the server has not spent.
442
+ */
386
443
  function toCredential(issuer) {
387
- const keyring = toKeyring(issuer);
388
- if (keyring) try {
389
- const held = keyring.getPassword();
390
- if (held) return {
391
- ...toRecord(held),
392
- backend: "keyring"
393
- };
394
- } catch {}
444
+ const fromKeyring = toKeyringCredential(issuer);
395
445
  const held = readFile()[issuer];
396
- return held === void 0 ? void 0 : {
446
+ const fromFile = held === void 0 ? void 0 : {
397
447
  ...held,
398
448
  backend: "file",
399
449
  path: toCredentialPath()
400
450
  };
451
+ if (!fromKeyring || !fromFile) return fromKeyring ?? fromFile;
452
+ return (fromFile.renewedAt ?? "") > (fromKeyring.renewedAt ?? "") ? fromFile : fromKeyring;
453
+ }
454
+ function toKeyringCredential(issuer) {
455
+ const keyring = toKeyring(issuer);
456
+ if (!keyring) return;
457
+ try {
458
+ const held = keyring.getPassword();
459
+ return held ? {
460
+ ...toRecord(held),
461
+ backend: "keyring"
462
+ } : void 0;
463
+ } catch {
464
+ return;
465
+ }
401
466
  }
402
467
  /** toRecord reads a stored entry, which older versions wrote as the bare token. */
403
468
  function toRecord(held) {
@@ -410,18 +475,25 @@ function toRecord(held) {
410
475
  }
411
476
  /** forget removes whatever is held for an issuer, in both places. */
412
477
  function forget(issuer) {
478
+ forgetKeyring(issuer);
479
+ forgetFile(issuer);
480
+ }
481
+ function forgetKeyring(issuer) {
413
482
  const keyring = toKeyring(issuer);
414
- if (keyring) try {
483
+ if (!keyring) return;
484
+ try {
415
485
  keyring.deletePassword();
416
486
  } catch {}
487
+ }
488
+ function forgetFile(issuer) {
417
489
  const held = readFile();
418
490
  if (held[issuer] === void 0) return;
419
- delete held[issuer];
420
- if (Object.keys(held).length === 0) {
491
+ const remaining = Object.fromEntries(Object.entries(held).filter(([name]) => name !== issuer));
492
+ if (Object.keys(remaining).length === 0) {
421
493
  rmSync(toCredentialPath(), { force: true });
422
494
  return;
423
495
  }
424
- writeFile(held);
496
+ writeFile(remaining);
425
497
  }
426
498
  /** toKeyring opens the OS keyring, or answers undefined where the platform has none. */
427
499
  function toKeyring(issuer) {
@@ -460,6 +532,8 @@ const STALE_MS = 3e4;
460
532
  const WAIT_MS$1 = 1e4;
461
533
  const RETRY_MS = 25;
462
534
  const DIRECTORY_MODE = 448;
535
+ /** What this process wrote into the lock, which is how it knows the lock is still its own. */
536
+ let heldBy;
463
537
  /** toLockPath names the lock every process coordinates credential writes through. */
464
538
  function toLockPath() {
465
539
  return join(dirname(toCredentialPath()), "credentials.lock");
@@ -471,17 +545,23 @@ function tryAcquire() {
471
545
  recursive: true,
472
546
  mode: DIRECTORY_MODE
473
547
  });
548
+ const mark = `${process.pid}:${randomUUID()}`;
474
549
  try {
475
550
  const handle = openSync(path, "wx");
476
- writeSync(handle, String(process.pid));
551
+ writeSync(handle, mark);
477
552
  closeSync(handle);
553
+ heldBy = mark;
478
554
  return true;
479
555
  } catch {
480
- return isStale(path) ? steal(path) : false;
556
+ return isStale(path) ? steal(path, mark) : false;
481
557
  }
482
558
  }
483
- /** release lets the next process in. */
559
+ /** release lets the next process in, and only ever removes this process's own lock. */
484
560
  function release$1() {
561
+ const mark = heldBy;
562
+ if (mark === void 0) return;
563
+ heldBy = void 0;
564
+ if (toMark(toLockPath()) !== mark) return;
485
565
  rmSync(toLockPath(), { force: true });
486
566
  }
487
567
  /**
@@ -508,9 +588,29 @@ function isStale(path) {
508
588
  return true;
509
589
  }
510
590
  }
511
- function steal(path) {
591
+ /**
592
+ * steal takes over a lock whose holder is gone. Two processes can reach this at once, so
593
+ * the winner is whichever mark survives in the file, not whichever removed it.
594
+ */
595
+ function steal(path, mark) {
512
596
  rmSync(path, { force: true });
513
- return tryAcquire();
597
+ try {
598
+ const handle = openSync(path, "wx");
599
+ writeSync(handle, mark);
600
+ closeSync(handle);
601
+ } catch {
602
+ return false;
603
+ }
604
+ if (toMark(path) !== mark) return false;
605
+ heldBy = mark;
606
+ return true;
607
+ }
608
+ function toMark(path) {
609
+ try {
610
+ return readFileSync(path, "utf8");
611
+ } catch {
612
+ return;
613
+ }
514
614
  }
515
615
  //#endregion
516
616
  //#region src/auth/session.ts
@@ -552,12 +652,52 @@ async function toAccessToken(settings, refreshToken) {
552
652
  const metadata = await toMetadata(settings.issuerUrl);
553
653
  return await withLock(async () => {
554
654
  const latest = toCredential(settings.issuerUrl)?.refreshToken ?? refreshToken;
555
- const tokens = await toTokensFromRefresh(metadata.token_endpoint, settings.clientId, latest);
556
- held.set(settings.issuerUrl, tokens);
557
- if (tokens.refreshToken && tokens.refreshToken !== latest) keep(settings.issuerUrl, tokens.refreshToken);
558
- return tokens;
655
+ try {
656
+ const tokens = toDated(await toTokensFromRefresh(metadata.token_endpoint, settings.clientId, latest));
657
+ held.set(settings.issuerUrl, tokens);
658
+ store(settings.issuerUrl, tokens, latest);
659
+ return tokens;
660
+ } catch (error) {
661
+ if (error instanceof GrantFailure && isDead(error.code)) {
662
+ forget(settings.issuerUrl);
663
+ throw new NoCredential(`the sign in for ${settings.issuerUrl} is no longer valid, so it was removed. Run hardfin login`);
664
+ }
665
+ throw error;
666
+ }
559
667
  });
560
668
  }
669
+ /**
670
+ * invalid_grant is the only refusal that says anything about the refresh token itself.
671
+ * invalid_client and unauthorized_client describe the client registration, which is server
672
+ * configuration and a setting a person can mistype, so a good credential survives them.
673
+ */
674
+ function isDead(code) {
675
+ return code === "invalid_grant";
676
+ }
677
+ /**
678
+ * toDated fills in when an access token expires. A token endpoint that states no expires_in
679
+ * would otherwise have this process refresh on every command, and every refresh spends a
680
+ * generation of the token family.
681
+ */
682
+ function toDated(tokens) {
683
+ if (tokens.expiresAt !== void 0) return tokens;
684
+ return {
685
+ ...tokens,
686
+ expiresAt: toClaims(tokens.accessToken).expiresAt
687
+ };
688
+ }
689
+ /**
690
+ * store writes what a rotation issued. The old token is spent either way, so failing to
691
+ * write the new one is worth saying out loud rather than failing a command that succeeded.
692
+ */
693
+ function store(issuer, tokens, previous) {
694
+ if (!tokens.refreshToken || tokens.refreshToken === previous) return;
695
+ try {
696
+ keep(issuer, tokens.refreshToken, tokens.refreshExpiresAt);
697
+ } catch (error) {
698
+ process.stderr.write(`warning: this sign in was renewed but could not be stored, so the next command will ask you to sign in again: ${error instanceof Error ? error.message : String(error)}\n`);
699
+ }
700
+ }
561
701
  /** forgetHeldTokens drops the access tokens this process is holding. */
562
702
  function forgetHeldTokens() {
563
703
  held.clear();
@@ -580,18 +720,23 @@ var RequestFailure = class extends Error {
580
720
  /** request calls one /v2 endpoint and returns the envelope it answered with. */
581
721
  async function request(options) {
582
722
  const url = new URL(`${options.apiUrl}${toLeadingSlash(options.path)}`);
583
- if (options.query) url.search = options.query.toString();
723
+ for (const [name, value] of options.query ?? []) url.searchParams.append(name, value);
584
724
  const headers = {
585
725
  [options.credential.header]: options.credential.value,
586
726
  "X-API-Version": API_VERSION,
587
- Accept: "application/json"
727
+ Accept: options.downloads ? "*/*" : "application/json"
588
728
  };
589
- if (options.body !== void 0) headers["Content-Type"] = "application/json";
729
+ if (options.body !== void 0 && options.form === void 0) headers["Content-Type"] = "application/json";
590
730
  const response = await fetch(url, {
591
731
  method: options.method,
592
732
  headers,
593
- body: options.body === void 0 ? void 0 : JSON.stringify(options.body)
733
+ body: options.form ?? (options.body === void 0 ? void 0 : JSON.stringify(options.body))
594
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
+ } };
595
740
  const envelope = toEnvelope(await response.text());
596
741
  if (!response.ok && envelope === void 0) throw new RequestFailure(response.status, [{
597
742
  error: toStatusMessage(response.status),
@@ -606,6 +751,11 @@ async function request(options) {
606
751
  }
607
752
  return envelope ?? { data: null };
608
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
+ }
609
759
  function toLeadingSlash(path) {
610
760
  return path.startsWith("/") ? path : `/${path}`;
611
761
  }
@@ -705,7 +855,7 @@ async function runApi(input) {
705
855
  writeData((await request({
706
856
  apiUrl: input.resolved.settings.apiUrl,
707
857
  credential: await toRequestCredential(input.resolved.settings),
708
- method: String(input.flags["method"] ?? "GET").toUpperCase(),
858
+ method: toText(input.flags["method"] ?? "GET").toUpperCase(),
709
859
  path,
710
860
  query,
711
861
  body
@@ -743,6 +893,132 @@ function toBody$1(source) {
743
893
  }
744
894
  }
745
895
  //#endregion
896
+ //#region src/command/completion.ts
897
+ const SHELLS = [
898
+ "bash",
899
+ "zsh",
900
+ "fish",
901
+ "powershell"
902
+ ];
903
+ const completionCommand = defineCommand({
904
+ name: "completion",
905
+ summary: "Print the shell script that completes hardfin commands",
906
+ description: "Writes a script for your shell. The script asks this CLI what may follow what you have typed, so completions never fall behind the commands.",
907
+ arguments: [{
908
+ name: "shell",
909
+ description: `The shell to write for: ${SHELLS.join(", ")}`,
910
+ required: true
911
+ }],
912
+ flags: [],
913
+ examples: [
914
+ {
915
+ description: "Complete in this shell, now",
916
+ command: "source <(hardfin completion zsh)"
917
+ },
918
+ {
919
+ description: "Complete in every new shell",
920
+ command: "hardfin completion zsh > ~/.hardfin-completion.zsh"
921
+ },
922
+ {
923
+ description: "Complete in bash",
924
+ command: "source <(hardfin completion bash)"
925
+ }
926
+ ],
927
+ run: runCompletion
928
+ });
929
+ /** The hidden command a completion script asks, which keeps one implementation for every shell. */
930
+ const completeCommand = defineCommand({
931
+ name: "__complete",
932
+ summary: "Answer what may follow the words typed so far",
933
+ hidden: true,
934
+ arguments: [{
935
+ name: "words",
936
+ description: "The words typed so far",
937
+ required: false,
938
+ variadic: true
939
+ }],
940
+ flags: [{
941
+ name: "json",
942
+ description: "Accepted for consistency, and ignored",
943
+ schema: z.boolean()
944
+ }],
945
+ examples: [],
946
+ run: async (input) => {
947
+ writeData(toCandidates(input.commands, input.args).join("\n"));
948
+ return ExitCode.OK;
949
+ }
950
+ });
951
+ /**
952
+ * toCandidates answers what may follow the words typed so far. A word starting with a dash
953
+ * asks for the current command's flags, and anything else asks for its subcommands.
954
+ */
955
+ function toCandidates(commands, words) {
956
+ const partial = words[words.length - 1] ?? "";
957
+ const walked = toWalked(commands, words.slice(0, -1));
958
+ if (partial.startsWith("-")) return toFlagNames(walked.command).filter((name) => name.startsWith(partial));
959
+ if (walked.isUnknown) return [];
960
+ return (walked.command?.subcommands ?? walked.remaining).map((command) => command.name).filter((name) => !name.startsWith("__")).filter((name) => name.startsWith(partial));
961
+ }
962
+ function toWalked(commands, words) {
963
+ let remaining = commands;
964
+ let command;
965
+ for (const word of words) {
966
+ if (word.startsWith("-") || command?.arguments.length) continue;
967
+ const found = remaining.find((entry) => entry.name === word);
968
+ if (!found) return {
969
+ command,
970
+ remaining,
971
+ isUnknown: true
972
+ };
973
+ command = found;
974
+ remaining = found.subcommands ?? [];
975
+ }
976
+ return {
977
+ command,
978
+ remaining,
979
+ isUnknown: false
980
+ };
981
+ }
982
+ function toFlagNames(command) {
983
+ return [...(command?.flags ?? []).map((flag) => `--${flag.name}`), "--help"];
984
+ }
985
+ async function runCompletion(input) {
986
+ const shell = input.args[0] ?? "";
987
+ if (!SHELLS.includes(shell)) {
988
+ writeFailure(`${shell || "no shell"} is not one this CLI writes for. Choose ${SHELLS.join(", ")}`, input.isJSON);
989
+ return ExitCode.USAGE;
990
+ }
991
+ writeData(toScript(shell));
992
+ return ExitCode.OK;
993
+ }
994
+ /** toScript writes a shell's completion, each one asking __complete for the candidates. */
995
+ function toScript(shell) {
996
+ if (shell === "bash") return `# hardfin completion for bash
997
+ _hardfin_complete() {
998
+ local words
999
+ words=("\${COMP_WORDS[@]:1}")
1000
+ COMPREPLY=($(hardfin __complete -- "\${words[@]}" 2>/dev/null))
1001
+ }
1002
+ complete -F _hardfin_complete hardfin`;
1003
+ if (shell === "zsh") return `# hardfin completion for zsh
1004
+ _hardfin_complete() {
1005
+ local -a candidates
1006
+ candidates=(\${(f)"$(hardfin __complete -- \${words[2,-1]} 2>/dev/null)"})
1007
+ compadd -a candidates
1008
+ }
1009
+ compdef _hardfin_complete hardfin`;
1010
+ if (shell === "fish") return `# hardfin completion for fish
1011
+ complete -c hardfin -f -a "(hardfin __complete -- (commandline -opc)[2..-1] 2>/dev/null)"`;
1012
+ return `# hardfin completion for PowerShell
1013
+ Register-ArgumentCompleter -Native -CommandName hardfin -ScriptBlock {
1014
+ param($wordToComplete, $commandAst, $cursorPosition)
1015
+ $words = $commandAst.CommandElements | Select-Object -Skip 1 | ForEach-Object { $_.ToString() }
1016
+ hardfin __complete -- @words 2>$null | ForEach-Object {
1017
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
1018
+ }
1019
+ }`;
1020
+ }
1021
+ //#endregion
746
1022
  //#region src/command/config.ts
747
1023
  const configCommand = defineCommand({
748
1024
  name: "config",
@@ -825,8 +1101,8 @@ function toOpeners(url) {
825
1101
  return [["xdg-open", [url]]];
826
1102
  }
827
1103
  /** openBrowser asks the desktop to show a URL, and reports whether anything took it. */
828
- function openBrowser(url) {
829
- for (const [command, args] of toOpeners(url)) if (spawnSync(command, args, {
1104
+ function openBrowser(url, launch = spawnSync) {
1105
+ for (const [command, args] of toOpeners(url)) if (launch(command, args, {
830
1106
  stdio: "ignore",
831
1107
  cwd: command.endsWith(".exe") ? "/mnt/c" : void 0,
832
1108
  timeout: 1e4
@@ -834,6 +1110,74 @@ function openBrowser(url) {
834
1110
  return false;
835
1111
  }
836
1112
  //#endregion
1113
+ //#region src/auth/device.ts
1114
+ const DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
1115
+ /** The interval a server gives is in seconds, and five more are added on a slow_down. */
1116
+ const DEFAULT_INTERVAL_SECONDS = 5;
1117
+ const SLOW_DOWN_SECONDS = 5;
1118
+ const DeviceResponse = z.looseObject({
1119
+ device_code: z.string(),
1120
+ user_code: z.string(),
1121
+ verification_uri: z.string(),
1122
+ verification_uri_complete: z.string().optional(),
1123
+ expires_in: z.number(),
1124
+ interval: z.number().optional()
1125
+ });
1126
+ /** toDeviceAuthorization asks for a code a person can type on another machine. */
1127
+ async function toDeviceAuthorization(endpoint, clientId, scope) {
1128
+ const response = await fetch(endpoint, {
1129
+ method: "POST",
1130
+ headers: {
1131
+ "Content-Type": "application/x-www-form-urlencoded",
1132
+ Accept: "application/json"
1133
+ },
1134
+ body: new URLSearchParams({
1135
+ client_id: clientId,
1136
+ scope
1137
+ })
1138
+ });
1139
+ const body = await response.json().catch(() => void 0);
1140
+ if (!response.ok) throw new GrantFailure(toText(body?.["error"] ?? `http_${response.status}`), body?.["error_description"] === void 0 ? void 0 : toText(body["error_description"]));
1141
+ const parsed = DeviceResponse.safeParse(body);
1142
+ if (!parsed.success) throw new GrantFailure("invalid_response", "the device endpoint did not answer with a code");
1143
+ return {
1144
+ deviceCode: parsed.data.device_code,
1145
+ userCode: parsed.data.user_code,
1146
+ verificationUri: parsed.data.verification_uri,
1147
+ verificationUriComplete: parsed.data.verification_uri_complete,
1148
+ expiresAt: Date.now() + parsed.data.expires_in * 1e3,
1149
+ intervalMs: (parsed.data.interval ?? DEFAULT_INTERVAL_SECONDS) * 1e3
1150
+ };
1151
+ }
1152
+ /**
1153
+ * toTokensFromDevice waits for the person to approve on another machine. The server answers
1154
+ * authorization_pending until they do, and slow_down when this asked too often.
1155
+ */
1156
+ async function toTokensFromDevice(tokenUrl, clientId, device, sleep = toSleep) {
1157
+ let intervalMs = device.intervalMs;
1158
+ for (;;) {
1159
+ if (Date.now() > device.expiresAt) throw new GrantFailure("expired_token", "the code expired before it was approved");
1160
+ await sleep(intervalMs);
1161
+ try {
1162
+ return await toTokens(tokenUrl, {
1163
+ grant_type: DEVICE_GRANT,
1164
+ client_id: clientId,
1165
+ device_code: device.deviceCode
1166
+ });
1167
+ } catch (error) {
1168
+ if (!(error instanceof GrantFailure)) throw error;
1169
+ if (error.code === "slow_down") {
1170
+ intervalMs += SLOW_DOWN_SECONDS * 1e3;
1171
+ continue;
1172
+ }
1173
+ if (error.code !== "authorization_pending") throw error;
1174
+ }
1175
+ }
1176
+ }
1177
+ function toSleep(ms) {
1178
+ return new Promise((resolve) => setTimeout(resolve, ms));
1179
+ }
1180
+ //#endregion
837
1181
  //#region src/auth/loopback.ts
838
1182
  /** The path the browser is sent back to, which the client metadata document registers. */
839
1183
  const CALLBACK_PATH = "/callback";
@@ -869,8 +1213,12 @@ async function toListener(timeoutMs) {
869
1213
  }, timeoutMs);
870
1214
  return {
871
1215
  redirectUri: `http://${HOST}:${server.address().port}${CALLBACK_PATH}`,
872
- callback: callback.finally(() => close(server, timer)),
873
- close: () => close(server, timer)
1216
+ callback: callback.finally(() => {
1217
+ close(server, timer);
1218
+ }),
1219
+ close: () => {
1220
+ close(server, timer);
1221
+ }
874
1222
  };
875
1223
  }
876
1224
  function toCallback(request) {
@@ -924,11 +1272,11 @@ function toClipboardCommand() {
924
1272
  return ["xclip", ["-selection", "clipboard"]];
925
1273
  }
926
1274
  /** copyToClipboard puts text on the clipboard, and reports whether anything took it. */
927
- function copyToClipboard(text) {
1275
+ function copyToClipboard(text, copy = spawnSync) {
928
1276
  const command = toClipboardCommand();
929
1277
  if (!command) return false;
930
1278
  const [name, args] = command;
931
- const result = spawnSync(name, args, { input: text });
1279
+ const result = copy(name, args, { input: text });
932
1280
  return result.error === void 0 && result.status === 0;
933
1281
  }
934
1282
  //#endregion
@@ -964,20 +1312,21 @@ function toPastedCallback(pasted) {
964
1312
  * toPrompt watches the keyboard while the browser is away. Pressing c copies the URL, and
965
1313
  * pasting a code finishes the sign in on a machine whose browser cannot reach this listener.
966
1314
  */
967
- function toPrompt(url) {
1315
+ function toPrompt(url, input = process.stdin, copy = copyToClipboard) {
968
1316
  let settle = () => {};
969
1317
  let fail = () => {};
970
1318
  const pasted = new Promise((resolve, reject) => {
971
1319
  settle = resolve;
972
1320
  fail = reject;
973
1321
  });
974
- const input = process.stdin;
1322
+ pasted.catch(() => {});
975
1323
  if (!input.isTTY) return {
976
1324
  pasted,
977
1325
  close: () => {}
978
1326
  };
979
1327
  let typed = "";
980
1328
  const onData = (chunk) => {
1329
+ const isKeystroke = chunk.length === 1;
981
1330
  for (const character of chunk) {
982
1331
  if (character === CTRL_C) {
983
1332
  fail(/* @__PURE__ */ new Error("sign in was cancelled"));
@@ -994,8 +1343,8 @@ function toPrompt(url) {
994
1343
  process.stderr.write("\b \b");
995
1344
  continue;
996
1345
  }
997
- if ((character === "c" || character === "C") && typed === "") {
998
- process.stderr.write(copyToClipboard(url) ? "Copied the URL to your clipboard\n" : "Nothing on this host takes a clipboard\n");
1346
+ if (isKeystroke && (character === "c" || character === "C") && typed === "") {
1347
+ process.stderr.write(copy(url) ? "Copied the URL to your clipboard\n" : "Nothing on this host takes a clipboard\n");
999
1348
  continue;
1000
1349
  }
1001
1350
  typed += character;
@@ -1039,19 +1388,31 @@ const loginCommand = defineCommand({
1039
1388
  description: "Print the URL instead of opening it",
1040
1389
  schema: z.boolean()
1041
1390
  },
1391
+ {
1392
+ name: "device",
1393
+ description: "Approve on another machine, by typing a code, with no listener on this one",
1394
+ schema: z.boolean()
1395
+ },
1042
1396
  {
1043
1397
  name: "json",
1044
1398
  description: "Print machine-readable output, which is the default when stdout is not a terminal",
1045
1399
  schema: z.boolean()
1046
1400
  }
1047
1401
  ],
1048
- examples: [{
1049
- description: "Sign in",
1050
- command: "hardfin login"
1051
- }, {
1052
- description: "Sign in over SSH, opening the URL yourself",
1053
- command: "hardfin login --no-browser"
1054
- }],
1402
+ examples: [
1403
+ {
1404
+ description: "Sign in",
1405
+ command: "hardfin login"
1406
+ },
1407
+ {
1408
+ description: "Sign in over SSH, opening the URL yourself",
1409
+ command: "hardfin login --no-browser"
1410
+ },
1411
+ {
1412
+ description: "Approve from your phone or another machine",
1413
+ command: "hardfin login --device"
1414
+ }
1415
+ ],
1055
1416
  run: runLogin
1056
1417
  });
1057
1418
  async function runLogin(input) {
@@ -1059,13 +1420,15 @@ async function runLogin(input) {
1059
1420
  try {
1060
1421
  const clientId = settings.clientId;
1061
1422
  const metadata = await toMetadata(settings.issuerUrl);
1423
+ const scope = toText(input.flags["scope"] ?? DEFAULT_SCOPES);
1424
+ if (input.flags["device"] === true) return await runDeviceLogin(input, metadata, scope);
1062
1425
  const listener = await toListener(WAIT_MS);
1063
1426
  const pkce = toPkce();
1064
1427
  const state = toState();
1065
1428
  const url = toAuthorizationUrl(metadata.authorization_endpoint, {
1066
1429
  clientId,
1067
1430
  redirectUri: listener.redirectUri,
1068
- scope: String(input.flags["scope"] ?? DEFAULT_SCOPES),
1431
+ scope,
1069
1432
  state,
1070
1433
  challenge: pkce.challenge
1071
1434
  });
@@ -1073,9 +1436,13 @@ async function runLogin(input) {
1073
1436
  process.stderr.write(opened ? `Opening your browser to sign in. If it did not open:\n\n${url}\n\n` : `Open this URL to sign in:\n\n${url}\n\n`);
1074
1437
  const prompt = toPrompt(url);
1075
1438
  if (process.stdin.isTTY) process.stderr.write("Press c to copy the URL, or paste the code or redirect URL here: ");
1076
- const callback = await Promise.race([listener.callback, prompt.pasted]);
1077
- listener.close();
1078
- prompt.close();
1439
+ let callback;
1440
+ try {
1441
+ callback = await Promise.race([listener.callback, prompt.pasted]);
1442
+ } finally {
1443
+ listener.close();
1444
+ prompt.close();
1445
+ }
1079
1446
  process.stderr.write(callback.error ? "\n" : "\nApproved, finishing the sign in\n");
1080
1447
  if (callback.error) {
1081
1448
  writeFailure(`sign in was refused: ${callback.error}${callback.errorDescription ? `, ${callback.errorDescription}` : ""}`, input.isJSON);
@@ -1093,31 +1460,7 @@ async function runLogin(input) {
1093
1460
  writeFailure("the browser came back without an authorization code", input.isJSON);
1094
1461
  return ExitCode.ERROR;
1095
1462
  }
1096
- const tokens = await toTokensFromCode(metadata.token_endpoint, clientId, callback.code, listener.redirectUri, pkce);
1097
- const refreshToken = tokens.refreshToken;
1098
- if (!refreshToken) {
1099
- writeFailure("the authorization server issued no refresh token, so this sign in cannot be kept", input.isJSON);
1100
- return ExitCode.ERROR;
1101
- }
1102
- const backend = await withLock(() => keep(settings.issuerUrl, refreshToken));
1103
- if (input.isJSON) {
1104
- writeData({
1105
- signedIn: true,
1106
- issuer: metadata.issuer,
1107
- scope: tokens.scope ?? null,
1108
- storedIn: backend
1109
- });
1110
- return ExitCode.OK;
1111
- }
1112
- const stored = backend === "keyring" ? "your OS keyring" : toCredentialPath();
1113
- writeData([
1114
- `Signed in to ${metadata.issuer}`,
1115
- `Scope ${tokens.scope ?? "as granted"}`,
1116
- `Stored in ${stored}`,
1117
- "",
1118
- "Run hardfin status to see what this CLI is using"
1119
- ].join("\n"));
1120
- return ExitCode.OK;
1463
+ return await toSignedIn(input, metadata, await toTokensFromCode(metadata.token_endpoint, clientId, callback.code, listener.redirectUri, pkce));
1121
1464
  } catch (error) {
1122
1465
  if (error instanceof DiscoveryFailure || error instanceof GrantFailure) {
1123
1466
  writeFailure(error.message, input.isJSON);
@@ -1127,6 +1470,61 @@ async function runLogin(input) {
1127
1470
  return ExitCode.ERROR;
1128
1471
  }
1129
1472
  }
1473
+ /** runDeviceLogin waits while the person approves on a machine that has a browser. */
1474
+ async function runDeviceLogin(input, metadata, scope) {
1475
+ const settings = input.resolved.settings;
1476
+ if (!metadata.device_authorization_endpoint) {
1477
+ writeFailure(`${metadata.issuer} does not offer the device grant, so sign in without --device`, input.isJSON);
1478
+ return ExitCode.ERROR;
1479
+ }
1480
+ const device = await toDeviceAuthorization(metadata.device_authorization_endpoint, settings.clientId, scope);
1481
+ const url = device.verificationUriComplete ?? device.verificationUri;
1482
+ const opened = input.flags["noBrowser"] !== true && !process.env["HARDFIN_NO_BROWSER"] && openBrowser(url);
1483
+ process.stderr.write([
1484
+ "",
1485
+ ` Code ${device.userCode}`,
1486
+ ` At ${device.verificationUri}`,
1487
+ "",
1488
+ opened ? "Opened your browser there. Waiting for approval\n" : "Open that page on any machine, and enter the code. Waiting for approval\n"
1489
+ ].join("\n"));
1490
+ const prompt = toPrompt(url);
1491
+ try {
1492
+ return await toSignedIn(input, metadata, await Promise.race([toTokensFromDevice(metadata.token_endpoint, settings.clientId, device), prompt.pasted.then(toCancelled)]));
1493
+ } finally {
1494
+ prompt.close();
1495
+ }
1496
+ }
1497
+ /** toCancelled ends a device sign in that the keyboard interrupted. */
1498
+ function toCancelled() {
1499
+ throw new Error("sign in was cancelled");
1500
+ }
1501
+ /** toSignedIn stores what a sign in issued, whichever flow issued it. */
1502
+ async function toSignedIn(input, metadata, tokens) {
1503
+ const refreshToken = tokens.refreshToken;
1504
+ if (!refreshToken) {
1505
+ writeFailure("the authorization server issued no refresh token, so this sign in cannot be kept", input.isJSON);
1506
+ return ExitCode.ERROR;
1507
+ }
1508
+ const backend = await withLock(() => keep(input.resolved.settings.issuerUrl, refreshToken, tokens.refreshExpiresAt));
1509
+ if (input.isJSON) {
1510
+ writeData({
1511
+ signedIn: true,
1512
+ issuer: metadata.issuer,
1513
+ scope: tokens.scope ?? null,
1514
+ storedIn: backend
1515
+ });
1516
+ return ExitCode.OK;
1517
+ }
1518
+ const stored = backend === "keyring" ? "your OS keyring" : toCredentialPath();
1519
+ writeData([
1520
+ `Signed in to ${metadata.issuer}`,
1521
+ `Scope ${tokens.scope ?? "as granted"}`,
1522
+ `Stored in ${stored}`,
1523
+ "",
1524
+ "Run hardfin status to see what this CLI is using"
1525
+ ].join("\n"));
1526
+ return ExitCode.OK;
1527
+ }
1130
1528
  /** toAuthorizationUrl builds the URL the person approves this CLI at. */
1131
1529
  function toAuthorizationUrl(endpoint, request) {
1132
1530
  const url = new URL(endpoint);
@@ -1178,7 +1576,9 @@ async function runLogout(input) {
1178
1576
  } catch {
1179
1577
  revoked = false;
1180
1578
  }
1181
- await withLock(() => forget(settings.issuerUrl));
1579
+ await withLock(() => {
1580
+ forget(settings.issuerUrl);
1581
+ });
1182
1582
  forgetHeldTokens();
1183
1583
  writeData({
1184
1584
  signedOut: true,
@@ -1188,55 +1588,319 @@ async function runLogout(input) {
1188
1588
  return ExitCode.OK;
1189
1589
  }
1190
1590
  //#endregion
1191
- //#region src/command/operation.ts
1192
- const INPUT_FLAG = {
1193
- name: "input",
1194
- description: "A file holding the JSON request body, or - for stdin",
1195
- valueName: "file",
1196
- schema: z.string()
1197
- };
1198
- const JSON_FLAG = {
1199
- name: "json",
1200
- description: "Print machine-readable output, which is the default when stdout is not a terminal",
1201
- schema: z.boolean()
1591
+ //#region src/mcp/server.ts
1592
+ const PROTOCOL_VERSION = "2025-06-18";
1593
+ const GUIDE_URI = "hardfin://guide";
1594
+ const COMMANDS_URI = "hardfin://commands";
1595
+ /**
1596
+ * One tool, because every tool a server lists sits in the agent's context for the whole
1597
+ * session. The commands themselves are resources, which cost nothing until one is read.
1598
+ */
1599
+ const TOOL = {
1600
+ name: "hardfin",
1601
+ description: "Run a Hardfin CLI command against the Hardfin API. Pass the arguments as a list, such as [\"asset\", \"list\", \"--limit\", \"5\"]. Run [\"--help\"] for the commands, or read the hardfin://guide resource.",
1602
+ inputSchema: {
1603
+ type: "object",
1604
+ properties: { args: {
1605
+ type: "array",
1606
+ items: { type: "string" },
1607
+ description: "The arguments to hardfin, without the program name"
1608
+ } },
1609
+ required: ["args"]
1610
+ }
1202
1611
  };
1203
- /** defineOperation turns one endpoint into the command that calls it. */
1204
- function defineOperation(operation) {
1205
- const flags = [...operation.queryFlags, JSON_FLAG];
1206
- if (operation.takesBody) flags.splice(flags.length - 1, 0, INPUT_FLAG);
1612
+ /** toResponse answers one request, and answers nothing to a notification. */
1613
+ async function toResponse(request, commands, run) {
1614
+ const answer = (result) => ({
1615
+ jsonrpc: "2.0",
1616
+ id: request.id ?? null,
1617
+ result
1618
+ });
1619
+ switch (request.method) {
1620
+ case "initialize": return answer({
1621
+ protocolVersion: toProtocolVersion(request.params),
1622
+ capabilities: {
1623
+ tools: {},
1624
+ resources: {}
1625
+ },
1626
+ serverInfo: {
1627
+ name: "hardfin",
1628
+ version: version$1
1629
+ }
1630
+ });
1631
+ case "tools/list": return answer({ tools: [TOOL] });
1632
+ case "resources/list": return answer({ resources: [{
1633
+ uri: GUIDE_URI,
1634
+ name: "Hardfin CLI guide",
1635
+ description: "Every command, its flags, and the exit codes",
1636
+ mimeType: "text/markdown"
1637
+ }, {
1638
+ uri: COMMANDS_URI,
1639
+ name: "Hardfin CLI commands",
1640
+ description: "The command tree as JSON",
1641
+ mimeType: "application/json"
1642
+ }] });
1643
+ case "resources/read": return answer(toResource(toText(request.params?.["uri"] ?? ""), commands));
1644
+ case "tools/call": return answer(await toToolResult(request.params, run));
1645
+ case "ping": return answer({});
1646
+ default:
1647
+ if (request.id === void 0 || request.id === null) return;
1648
+ return {
1649
+ jsonrpc: "2.0",
1650
+ id: request.id,
1651
+ error: {
1652
+ code: -32601,
1653
+ message: `${request.method} is not a method this server offers`
1654
+ }
1655
+ };
1656
+ }
1657
+ }
1658
+ function toProtocolVersion(params) {
1659
+ const asked = params?.["protocolVersion"];
1660
+ return typeof asked === "string" ? asked : PROTOCOL_VERSION;
1661
+ }
1662
+ function toResource(uri, commands) {
1663
+ if (uri === GUIDE_URI) return { contents: [{
1664
+ uri,
1665
+ mimeType: "text/markdown",
1666
+ text: toGuide(commands, version$1)
1667
+ }] };
1668
+ if (uri === COMMANDS_URI) return { contents: [{
1669
+ uri,
1670
+ mimeType: "application/json",
1671
+ text: JSON.stringify(toTree(commands), null, 2)
1672
+ }] };
1207
1673
  return {
1208
- name: operation.name,
1209
- summary: operation.summary,
1210
- description: operation.description ?? operation.summary,
1211
- arguments: operation.pathParameters,
1212
- flags,
1213
- examples: [],
1214
- run: (input) => runOperation(operation, input)
1674
+ contents: [],
1675
+ isError: true
1215
1676
  };
1216
1677
  }
1217
- async function runOperation(operation, input) {
1218
- const path = toPath(operation, input.args);
1219
- if (path === void 0) {
1220
- writeFailure(`this command takes ${operation.pathParameters.length} argument(s)`, input.isJSON);
1221
- return ExitCode.USAGE;
1222
- }
1223
- let body;
1224
- if (typeof input.flags["input"] === "string") {
1225
- body = toBody(input.flags["input"]);
1226
- if (body === void 0) {
1227
- writeFailure(`${input.flags["input"]} does not hold JSON`, input.isJSON);
1228
- return ExitCode.USAGE;
1229
- }
1230
- }
1678
+ /** toTree names every command and what it takes, without the schemas a tool list would carry. */
1679
+ function toTree(commands) {
1680
+ return commands.map((command) => ({
1681
+ name: command.name,
1682
+ summary: command.summary,
1683
+ arguments: command.arguments.map((argument) => argument.name),
1684
+ flags: command.flags.map((flag) => flag.valueName ? `--${flag.name} <${flag.valueName}>` : `--${flag.name}`),
1685
+ subcommands: command.subcommands ? toTree(command.subcommands) : void 0
1686
+ }));
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
+ ]);
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
+ };
1703
+ const args = (params?.["arguments"])?.args;
1704
+ if (!Array.isArray(args) || args.some((entry) => typeof entry !== "string")) return {
1705
+ content: [{
1706
+ type: "text",
1707
+ text: "args must be a list of strings, such as [\"asset\", \"list\"]"
1708
+ }],
1709
+ isError: true
1710
+ };
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);
1720
+ return {
1721
+ content: [{
1722
+ type: "text",
1723
+ text: [outcome.stdout, outcome.stderr].filter((part) => part.trim() !== "").join("\n") || `hardfin exited ${outcome.code}`
1724
+ }],
1725
+ isError: outcome.code !== 0
1726
+ };
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) {
1231
1730
  try {
1232
- writeData((await request({
1233
- apiUrl: input.resolved.settings.apiUrl,
1234
- credential: await toRequestCredential(input.resolved.settings),
1235
- method: operation.method,
1236
- path,
1731
+ return JSON.parse(line).id ?? null;
1732
+ } catch {
1733
+ return null;
1734
+ }
1735
+ }
1736
+ /** toCliRunner runs the CLI itself, so a tool call parses exactly as a terminal would. */
1737
+ function toCliRunner() {
1738
+ return (args) => new Promise((resolve) => {
1739
+ const child = spawn(process.execPath, [process.argv[1] ?? "", ...args], { env: {
1740
+ ...process.env,
1741
+ HARDFIN_NO_BROWSER: "1"
1742
+ } });
1743
+ let stdout = "";
1744
+ let stderr = "";
1745
+ child.stdout.on("data", (chunk) => {
1746
+ stdout += chunk.toString();
1747
+ });
1748
+ child.stderr.on("data", (chunk) => {
1749
+ stderr += chunk.toString();
1750
+ });
1751
+ child.on("close", (code) => {
1752
+ resolve({
1753
+ stdout,
1754
+ stderr,
1755
+ code: code ?? 1
1756
+ });
1757
+ });
1758
+ });
1759
+ }
1760
+ /** serve answers requests on stdin until the client closes it. */
1761
+ async function serve(commands, run = toCliRunner()) {
1762
+ const lines = createInterface({ input: process.stdin });
1763
+ for await (const line of lines) {
1764
+ if (line.trim() === "") continue;
1765
+ let response;
1766
+ try {
1767
+ response = await toResponse(JSON.parse(line), commands, run);
1768
+ } catch (error) {
1769
+ response = {
1770
+ jsonrpc: "2.0",
1771
+ id: toRequestId(line),
1772
+ error: {
1773
+ code: -32603,
1774
+ message: error instanceof Error ? error.message : String(error)
1775
+ }
1776
+ };
1777
+ }
1778
+ if (response) process.stdout.write(`${JSON.stringify(response)}\n`);
1779
+ }
1780
+ }
1781
+ //#endregion
1782
+ //#region src/command/mcp.ts
1783
+ const mcpCommand = defineCommand({
1784
+ name: "mcp",
1785
+ summary: "Serve this CLI to an agent over the Model Context Protocol",
1786
+ description: "Speaks the Model Context Protocol on standard input and output. It offers one tool, because every tool an agent is told about occupies its context for the whole session, and serves the commands as resources the agent reads only when it needs them.",
1787
+ arguments: [],
1788
+ flags: [{
1789
+ name: "json",
1790
+ description: "Accepted for consistency, and ignored, because the protocol decides the output",
1791
+ schema: z.boolean()
1792
+ }],
1793
+ examples: [{
1794
+ description: "Register with an agent",
1795
+ command: "hardfin mcp"
1796
+ }, {
1797
+ description: "Add it to Claude Code",
1798
+ command: "claude mcp add hardfin -- hardfin mcp"
1799
+ }],
1800
+ run: runMcp
1801
+ });
1802
+ async function runMcp(input) {
1803
+ await serve(input.commands);
1804
+ return ExitCode.OK;
1805
+ }
1806
+ //#endregion
1807
+ //#region src/command/operation.ts
1808
+ /**
1809
+ * toEnum takes a value in any case, as the API does, and answers the spelling the document
1810
+ * lists, which is what a response always uses.
1811
+ */
1812
+ function toEnum(values) {
1813
+ return z.string().transform((value) => values.find((allowed) => allowed.toLowerCase() === value.toLowerCase()) ?? value).pipe(z.enum(values));
1814
+ }
1815
+ const UNSET_FLAG = {
1816
+ name: "unset",
1817
+ description: "A field to clear, named as its flag is, repeatable",
1818
+ valueName: "field",
1819
+ repeatable: true,
1820
+ schema: z.array(z.string())
1821
+ };
1822
+ const FILE_FLAG = {
1823
+ name: "file",
1824
+ description: "The file to upload",
1825
+ valueName: "path",
1826
+ schema: z.string()
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
+ };
1834
+ const JSON_FLAG = {
1835
+ name: "json",
1836
+ description: "Print machine-readable output, which is the default when stdout is not a terminal",
1837
+ schema: z.boolean()
1838
+ };
1839
+ /** defineOperation turns one endpoint into the command that calls it. */
1840
+ function defineOperation(operation) {
1841
+ const flags = [
1842
+ ...operation.queryFlags,
1843
+ ...operation.bodyFlags,
1844
+ ...operation.bodyFlags.some((flag) => flag.nullable) ? [UNSET_FLAG] : [],
1845
+ ...operation.upload ? [...operation.upload.fields, FILE_FLAG] : [],
1846
+ ...operation.downloads ? [OUTPUT_FLAG] : [],
1847
+ JSON_FLAG
1848
+ ];
1849
+ return {
1850
+ name: operation.name,
1851
+ summary: operation.summary,
1852
+ description: operation.description ?? operation.summary,
1853
+ arguments: operation.pathParameters,
1854
+ flags,
1855
+ examples: operation.example ? [{
1856
+ description: operation.summary,
1857
+ command: operation.example
1858
+ }] : [],
1859
+ endpoint: {
1860
+ method: operation.method,
1861
+ path: operation.path
1862
+ },
1863
+ run: (input) => runOperation(operation, input)
1864
+ };
1865
+ }
1866
+ async function runOperation(operation, input) {
1867
+ const path = toPath(operation, input.args);
1868
+ if (path === void 0) {
1869
+ writeFailure(`this command takes ${operation.pathParameters.length} argument(s)`, input.isJSON);
1870
+ return ExitCode.USAGE;
1871
+ }
1872
+ const missing = operation.bodyFlags.concat(operation.upload?.fields ?? []).filter((flag) => flag.required && input.flags[toOptionKey(flag.name)] === void 0);
1873
+ if (missing.length > 0) {
1874
+ writeFailure(`this command needs ${missing.map((flag) => `--${flag.name}`).join(", ")}`, input.isJSON);
1875
+ return ExitCode.USAGE;
1876
+ }
1877
+ const body = toBody(operation, input.flags);
1878
+ if (body instanceof Error) {
1879
+ writeFailure(body.message, input.isJSON);
1880
+ return ExitCode.USAGE;
1881
+ }
1882
+ let form;
1883
+ if (operation.upload) {
1884
+ const built = await toForm(operation.upload, input.flags);
1885
+ if (built instanceof Error) {
1886
+ writeFailure(built.message, input.isJSON);
1887
+ return ExitCode.USAGE;
1888
+ }
1889
+ form = built;
1890
+ }
1891
+ try {
1892
+ const envelope = await request({
1893
+ apiUrl: input.resolved.settings.apiUrl,
1894
+ credential: await toRequestCredential(input.resolved.settings),
1895
+ method: operation.method,
1896
+ path,
1237
1897
  query: toQuery(operation, input.flags),
1238
- body
1239
- })).data);
1898
+ body,
1899
+ form,
1900
+ downloads: operation.downloads
1901
+ });
1902
+ if (operation.downloads) return toWritten(envelope.data, input);
1903
+ writeData(envelope.data);
1240
1904
  return ExitCode.OK;
1241
1905
  } catch (error) {
1242
1906
  if (error instanceof NoCredential) {
@@ -1251,6 +1915,29 @@ async function runOperation(operation, input) {
1251
1915
  return ExitCode.ERROR;
1252
1916
  }
1253
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
+ }
1254
1941
  /** toPath fills the path template from the positional arguments, in order. */
1255
1942
  function toPath(operation, args) {
1256
1943
  if (args.length !== operation.pathParameters.length) return;
@@ -1264,7 +1951,9 @@ function toQuery(operation, flags) {
1264
1951
  for (const flag of operation.queryFlags) {
1265
1952
  const value = flags[toOptionKey(flag.name)];
1266
1953
  if (value === void 0) continue;
1267
- for (const entry of Array.isArray(value) ? value : [value]) query.append(flag.queryName, String(entry));
1954
+ const parsed = flag.schema.safeParse(value);
1955
+ const carried = parsed.success ? parsed.data : value;
1956
+ for (const entry of Array.isArray(carried) ? carried : [carried]) query.append(flag.queryName, toText(entry));
1268
1957
  }
1269
1958
  return query;
1270
1959
  }
@@ -1272,13 +1961,83 @@ function toQuery(operation, flags) {
1272
1961
  function toOptionKey(name) {
1273
1962
  return name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
1274
1963
  }
1275
- function toBody(source) {
1276
- const text = source === "-" ? readFileSync(0, "utf8") : readFileSync(source, "utf8");
1964
+ /** toForm builds the file part and its fields, which an upload endpoint takes. */
1965
+ async function toForm(upload, flags) {
1966
+ const path = flags["file"];
1967
+ if (typeof path !== "string") return /* @__PURE__ */ new Error("this command needs --file, the file to upload");
1968
+ const form = new FormData();
1277
1969
  try {
1278
- return JSON.parse(text);
1970
+ form.append(upload.filePart, await openAsBlob(path), basename(path));
1279
1971
  } catch {
1280
- return;
1972
+ return /* @__PURE__ */ new Error(`${path} cannot be read`);
1973
+ }
1974
+ for (const field of upload.fields) {
1975
+ const value = flags[toOptionKey(field.name)];
1976
+ if (value !== void 0) {
1977
+ const parsed = field.schema.safeParse(value);
1978
+ form.append(field.jsonPath[0] ?? field.name, toText(parsed.success ? parsed.data : value));
1979
+ }
1980
+ }
1981
+ return form;
1982
+ }
1983
+ /** toBody builds the request body from the flags, one field at a time. */
1984
+ function toBody(operation, flags) {
1985
+ const body = {};
1986
+ let hasField = false;
1987
+ for (const flag of operation.bodyFlags) {
1988
+ const value = flags[toOptionKey(flag.name)];
1989
+ if (value === void 0) continue;
1990
+ if (flag.element) {
1991
+ const elements = toElements(flag, Array.isArray(value) ? value.map(toText) : [toText(value)]);
1992
+ if (elements instanceof Error) return elements;
1993
+ set(body, flag.jsonPath, elements);
1994
+ hasField = true;
1995
+ continue;
1996
+ }
1997
+ const parsed = flag.schema.safeParse(value);
1998
+ set(body, flag.jsonPath, parsed.success ? parsed.data : value);
1999
+ hasField = true;
2000
+ }
2001
+ const cleared = toCleared(operation, flags, body);
2002
+ if (cleared instanceof Error) return cleared;
2003
+ return hasField || cleared ? body : void 0;
2004
+ }
2005
+ /** toCleared sends null for each field named by --unset, which is how a field is cleared. */
2006
+ function toCleared(operation, flags, body) {
2007
+ const named = flags["unset"];
2008
+ const names = Array.isArray(named) ? named.map(toText) : named === void 0 ? [] : [toText(named)];
2009
+ for (const name of names) {
2010
+ const flag = operation.bodyFlags.find((entry) => entry.name === name.replace(/^--/, ""));
2011
+ if (!flag) return /* @__PURE__ */ new Error(`--unset names no field called ${name}`);
2012
+ if (!flag.nullable) return /* @__PURE__ */ new Error(`--${flag.name} cannot be cleared, because the API does not accept null for it`);
2013
+ set(body, flag.jsonPath, null);
2014
+ }
2015
+ return names.length > 0;
2016
+ }
2017
+ /** toElements reads the key=value pairs a repeated flag carries for one array element. */
2018
+ function toElements(flag, values) {
2019
+ const elements = [];
2020
+ for (const value of values) {
2021
+ const element = {};
2022
+ for (const pair of value.split(",")) {
2023
+ const split = pair.indexOf("=");
2024
+ if (split < 1) return /* @__PURE__ */ new Error(`--${flag.name} takes ${flag.element?.join("=, ")}=, such as --${flag.name} ${flag.element?.[0]}=value`);
2025
+ const key = pair.slice(0, split);
2026
+ if (flag.element && !flag.element.includes(key)) return /* @__PURE__ */ new Error(`--${flag.name} has no field ${key}. It takes ${flag.element.join(", ")}`);
2027
+ element[key] = pair.slice(split + 1);
2028
+ }
2029
+ elements.push(element);
1281
2030
  }
2031
+ return elements;
2032
+ }
2033
+ /** set writes a value at its path, building the objects a nested field needs. */
2034
+ function set(body, path, value) {
2035
+ let holder = body;
2036
+ for (const name of path.slice(0, -1)) {
2037
+ holder[name] = holder[name] ?? {};
2038
+ holder = holder[name];
2039
+ }
2040
+ holder[path[path.length - 1] ?? ""] = value;
1282
2041
  }
1283
2042
  //#endregion
1284
2043
  //#region src/command/surface.generated.ts
@@ -1293,6 +2052,7 @@ const surfaceCommands = [
1293
2052
  defineOperation({
1294
2053
  name: "list",
1295
2054
  summary: "Get asset listing",
2055
+ example: "hardfin asset list --limit 10",
1296
2056
  method: "GET",
1297
2057
  path: "/asset",
1298
2058
  pathParameters: [],
@@ -1302,21 +2062,21 @@ const surfaceCommands = [
1302
2062
  queryName: "page",
1303
2063
  description: "The page to return, starting at 1",
1304
2064
  valueName: "number",
1305
- schema: z.coerce.number()
2065
+ schema: z.coerce.number().int()
1306
2066
  },
1307
2067
  {
1308
2068
  name: "limit",
1309
2069
  queryName: "limit",
1310
2070
  description: "The number of records per page, from 1 to 100",
1311
2071
  valueName: "number",
1312
- schema: z.coerce.number()
2072
+ schema: z.coerce.number().int().min(1).max(100)
1313
2073
  },
1314
2074
  {
1315
2075
  name: "sort-by",
1316
2076
  queryName: "sortBy",
1317
2077
  description: "The field to sort by",
1318
2078
  valueName: "value",
1319
- schema: z.enum([
2079
+ schema: toEnum([
1320
2080
  "serial",
1321
2081
  "project",
1322
2082
  "item",
@@ -1330,14 +2090,14 @@ const surfaceCommands = [
1330
2090
  queryName: "sortOrder",
1331
2091
  description: "The sort direction",
1332
2092
  valueName: "value",
1333
- schema: z.enum(["ASC", "DESC"])
2093
+ schema: toEnum(["ASC", "DESC"])
1334
2094
  },
1335
2095
  {
1336
2096
  name: "archived",
1337
2097
  queryName: "archived",
1338
2098
  description: "Whether to return unarchived records, archived records, or all of them",
1339
2099
  valueName: "value",
1340
- schema: z.enum([
2100
+ schema: toEnum([
1341
2101
  "all",
1342
2102
  "false",
1343
2103
  "true"
@@ -1404,7 +2164,7 @@ const surfaceCommands = [
1404
2164
  description: "The functional statuses to list, where SCRAPPED lists scrapped assets",
1405
2165
  valueName: "value",
1406
2166
  repeatable: true,
1407
- schema: z.array(z.enum([
2167
+ schema: z.array(toEnum([
1408
2168
  "FUNCTIONAL",
1409
2169
  "NEEDS_REVIEW",
1410
2170
  "NON-FUNCTIONAL",
@@ -1417,7 +2177,7 @@ const surfaceCommands = [
1417
2177
  description: "The transit statuses to list",
1418
2178
  valueName: "value",
1419
2179
  repeatable: true,
1420
- schema: z.array(z.enum([
2180
+ schema: z.array(toEnum([
1421
2181
  "IN_TRANSIT",
1422
2182
  "IN_TRANSIT_TO_FIELD",
1423
2183
  "IN_TRANSIT_TO_INVENTORY",
@@ -1469,23 +2229,192 @@ const surfaceCommands = [
1469
2229
  queryName: "scrapped",
1470
2230
  description: "Whether to list unscrapped assets, scrapped assets, or all of them",
1471
2231
  valueName: "value",
1472
- schema: z.enum([
2232
+ schema: toEnum([
1473
2233
  "all",
1474
2234
  "false",
1475
2235
  "true"
1476
2236
  ])
1477
2237
  }
1478
2238
  ],
1479
- takesBody: false
2239
+ bodyFlags: []
1480
2240
  }),
1481
2241
  defineOperation({
1482
2242
  name: "create",
1483
2243
  summary: "Create asset",
2244
+ example: "hardfin asset create --item-id <value> --serial <value>",
1484
2245
  method: "POST",
1485
2246
  path: "/asset",
1486
2247
  pathParameters: [],
1487
2248
  queryFlags: [],
1488
- takesBody: true
2249
+ bodyFlags: [
2250
+ {
2251
+ name: "allocated-indirect",
2252
+ jsonPath: ["allocatedIndirect"],
2253
+ description: "One unit's share of overhead, a cost component",
2254
+ valueName: "value",
2255
+ nullable: true,
2256
+ schema: z.string()
2257
+ },
2258
+ {
2259
+ name: "bill-of-materials",
2260
+ jsonPath: ["billOfMaterials"],
2261
+ description: "The parts cost of one unit, a cost component",
2262
+ valueName: "value",
2263
+ nullable: true,
2264
+ schema: z.string()
2265
+ },
2266
+ {
2267
+ name: "depreciation-model",
2268
+ jsonPath: ["depreciationModel"],
2269
+ description: "The method one unit is depreciated by, or null to clear it",
2270
+ valueName: "value",
2271
+ schema: toEnum([
2272
+ "DOUBLE_DECLINING",
2273
+ "STRAIGHT_LINE",
2274
+ "SUM_YEAR",
2275
+ "UNIT_OF_PRODUCTION"
2276
+ ])
2277
+ },
2278
+ {
2279
+ name: "description",
2280
+ jsonPath: ["description"],
2281
+ description: "A free-form description of the asset",
2282
+ valueName: "value",
2283
+ nullable: true,
2284
+ schema: z.string()
2285
+ },
2286
+ {
2287
+ name: "direct-labor",
2288
+ jsonPath: ["directLabor"],
2289
+ description: "The labor cost to build one unit, a cost component",
2290
+ valueName: "value",
2291
+ nullable: true,
2292
+ schema: z.string()
2293
+ },
2294
+ {
2295
+ name: "freight-inbound",
2296
+ jsonPath: ["freightInbound"],
2297
+ description: "The shipping cost to receive one unit, a cost component",
2298
+ valueName: "value",
2299
+ nullable: true,
2300
+ schema: z.string()
2301
+ },
2302
+ {
2303
+ name: "freight-outbound",
2304
+ jsonPath: ["freightOutbound"],
2305
+ description: "The shipping cost to deploy one unit, a deployment cost component",
2306
+ valueName: "value",
2307
+ nullable: true,
2308
+ schema: z.string()
2309
+ },
2310
+ {
2311
+ name: "functional-status",
2312
+ jsonPath: ["functionalStatus"],
2313
+ description: "The asset's starting functional status, FUNCTIONAL when absent, which cannot be SCRAPPED",
2314
+ valueName: "value",
2315
+ nullable: true,
2316
+ schema: toEnum([
2317
+ "FUNCTIONAL",
2318
+ "NEEDS_REVIEW",
2319
+ "NON-FUNCTIONAL",
2320
+ "SCRAPPED"
2321
+ ])
2322
+ },
2323
+ {
2324
+ name: "in-inventory-date",
2325
+ jsonPath: ["inInventoryDate"],
2326
+ description: "The day the asset entered inventory, which cannot be in the future",
2327
+ valueName: "value",
2328
+ schema: z.string()
2329
+ },
2330
+ {
2331
+ name: "in-service-date",
2332
+ jsonPath: ["inServiceDate"],
2333
+ description: "The day the asset was put into service, which Hardfin sets itself when absent",
2334
+ valueName: "value",
2335
+ nullable: true,
2336
+ schema: z.string()
2337
+ },
2338
+ {
2339
+ name: "installation",
2340
+ jsonPath: ["installation"],
2341
+ description: "The cost to install one unit, a deployment cost component",
2342
+ valueName: "value",
2343
+ nullable: true,
2344
+ schema: z.string()
2345
+ },
2346
+ {
2347
+ name: "interest",
2348
+ jsonPath: ["interest"],
2349
+ description: "The financing cost of one unit, a cost component",
2350
+ valueName: "value",
2351
+ nullable: true,
2352
+ schema: z.string()
2353
+ },
2354
+ {
2355
+ name: "item-id",
2356
+ jsonPath: ["itemId"],
2357
+ description: "The ID of the catalog item the asset is a unit of",
2358
+ valueName: "value",
2359
+ required: true,
2360
+ schema: z.string()
2361
+ },
2362
+ {
2363
+ name: "location-id",
2364
+ jsonPath: ["locationId"],
2365
+ description: "The ID of the location the asset enters inventory at",
2366
+ valueName: "value",
2367
+ schema: z.string()
2368
+ },
2369
+ {
2370
+ name: "salvage-value",
2371
+ jsonPath: ["salvageValue"],
2372
+ description: "The value one unit keeps at the end of its useful life, or null to clear it",
2373
+ valueName: "value",
2374
+ nullable: true,
2375
+ schema: z.string()
2376
+ },
2377
+ {
2378
+ name: "serial",
2379
+ jsonPath: ["serial"],
2380
+ description: "The asset's serial number, unique within its item",
2381
+ valueName: "value",
2382
+ required: true,
2383
+ schema: z.string()
2384
+ },
2385
+ {
2386
+ name: "simple-cost-basis",
2387
+ jsonPath: ["simpleCostBasis"],
2388
+ description: "A single cost for one unit, which cannot be sent together with the cost components",
2389
+ valueName: "value",
2390
+ nullable: true,
2391
+ schema: z.string()
2392
+ },
2393
+ {
2394
+ name: "tariffs",
2395
+ jsonPath: ["tariffs"],
2396
+ description: "The import duty paid on one unit, a cost component",
2397
+ valueName: "value",
2398
+ nullable: true,
2399
+ schema: z.string()
2400
+ },
2401
+ {
2402
+ name: "tax",
2403
+ jsonPath: ["tax"],
2404
+ description: "The tax paid on one unit, a cost component",
2405
+ valueName: "value",
2406
+ nullable: true,
2407
+ schema: z.string()
2408
+ },
2409
+ {
2410
+ name: "useful-life",
2411
+ jsonPath: ["usefulLife"],
2412
+ description: "The number of months one unit is depreciated over, or null to clear it",
2413
+ valueName: "number",
2414
+ nullable: true,
2415
+ schema: z.coerce.number().int()
2416
+ }
2417
+ ]
1489
2418
  }),
1490
2419
  {
1491
2420
  name: "move",
@@ -1502,11 +2431,28 @@ const surfaceCommands = [
1502
2431
  subcommands: [defineOperation({
1503
2432
  name: "create",
1504
2433
  summary: "Execute asset move",
2434
+ example: "hardfin asset move execute create",
1505
2435
  method: "POST",
1506
2436
  path: "/asset/move/execute",
1507
2437
  pathParameters: [],
1508
2438
  queryFlags: [],
1509
- takesBody: true
2439
+ bodyFlags: [{
2440
+ name: "move",
2441
+ jsonPath: ["moves"],
2442
+ description: "The moves to carry out",
2443
+ valueName: "assetId=,deliverAt=",
2444
+ repeatable: true,
2445
+ element: [
2446
+ "assetId",
2447
+ "deliverAt",
2448
+ "deliverAtTimezone",
2449
+ "destinationId",
2450
+ "originId",
2451
+ "shipAt",
2452
+ "shipAtTimezone"
2453
+ ],
2454
+ schema: z.array(z.string())
2455
+ }]
1510
2456
  })]
1511
2457
  }, {
1512
2458
  name: "plan",
@@ -1517,17 +2463,32 @@ const surfaceCommands = [
1517
2463
  subcommands: [defineOperation({
1518
2464
  name: "create",
1519
2465
  summary: "Plan asset move",
2466
+ example: "hardfin asset move plan create",
1520
2467
  method: "POST",
1521
2468
  path: "/asset/move/plan",
1522
2469
  pathParameters: [],
1523
2470
  queryFlags: [],
1524
- takesBody: true
2471
+ bodyFlags: [{
2472
+ name: "move",
2473
+ jsonPath: ["moves"],
2474
+ description: "The moves to plan",
2475
+ valueName: "assetId=,deliverAt=",
2476
+ repeatable: true,
2477
+ element: [
2478
+ "assetId",
2479
+ "deliverAt",
2480
+ "id",
2481
+ "shipAt"
2482
+ ],
2483
+ schema: z.array(z.string())
2484
+ }]
1525
2485
  })]
1526
2486
  }]
1527
2487
  },
1528
2488
  defineOperation({
1529
2489
  name: "get",
1530
2490
  summary: "Get asset",
2491
+ example: "hardfin asset get ast_4f9xk2mq7plr8stz",
1531
2492
  method: "GET",
1532
2493
  path: "/asset/{assetKey}",
1533
2494
  pathParameters: [{
@@ -1536,11 +2497,12 @@ const surfaceCommands = [
1536
2497
  required: true
1537
2498
  }],
1538
2499
  queryFlags: [],
1539
- takesBody: false
2500
+ bodyFlags: []
1540
2501
  }),
1541
2502
  defineOperation({
1542
2503
  name: "update",
1543
2504
  summary: "Patch asset",
2505
+ example: "hardfin asset update ast_4f9xk2mq7plr8stz",
1544
2506
  method: "PATCH",
1545
2507
  path: "/asset/{assetKey}",
1546
2508
  pathParameters: [{
@@ -1549,7 +2511,62 @@ const surfaceCommands = [
1549
2511
  required: true
1550
2512
  }],
1551
2513
  queryFlags: [],
1552
- takesBody: true
2514
+ bodyFlags: [
2515
+ {
2516
+ name: "description",
2517
+ jsonPath: ["description"],
2518
+ description: "The asset's new description, or null to clear it",
2519
+ valueName: "value",
2520
+ nullable: true,
2521
+ schema: z.string()
2522
+ },
2523
+ {
2524
+ name: "functional-status",
2525
+ jsonPath: ["functionalStatus"],
2526
+ description: "The asset's new functional status, which cannot be SCRAPPED because scrapping has its own endpoint",
2527
+ valueName: "value",
2528
+ nullable: true,
2529
+ schema: toEnum([
2530
+ "FUNCTIONAL",
2531
+ "NEEDS_REVIEW",
2532
+ "NON-FUNCTIONAL",
2533
+ "SCRAPPED"
2534
+ ])
2535
+ },
2536
+ {
2537
+ name: "in-inventory-date",
2538
+ jsonPath: ["inInventoryDate"],
2539
+ description: "The day the asset entered inventory, which cannot be in the future",
2540
+ valueName: "value",
2541
+ nullable: true,
2542
+ schema: z.string()
2543
+ },
2544
+ {
2545
+ name: "initial-location-id",
2546
+ jsonPath: ["initialLocationId"],
2547
+ description: "The ID of the location the asset entered inventory at",
2548
+ valueName: "value",
2549
+ nullable: true,
2550
+ schema: z.string()
2551
+ },
2552
+ {
2553
+ name: "metadata",
2554
+ jsonPath: ["metadata"],
2555
+ description: "New values for the asset's custom fields, each naming its field",
2556
+ valueName: "fieldId=,value=",
2557
+ repeatable: true,
2558
+ element: ["fieldId", "value"],
2559
+ schema: z.array(z.string())
2560
+ },
2561
+ {
2562
+ name: "serial",
2563
+ jsonPath: ["serial"],
2564
+ description: "The asset's new serial number, which cannot be empty",
2565
+ valueName: "value",
2566
+ nullable: true,
2567
+ schema: z.string()
2568
+ }
2569
+ ]
1553
2570
  }),
1554
2571
  {
1555
2572
  name: "accounting",
@@ -1560,6 +2577,7 @@ const surfaceCommands = [
1560
2577
  subcommands: [defineOperation({
1561
2578
  name: "update",
1562
2579
  summary: "Update asset accounting",
2580
+ example: "hardfin asset accounting update ast_4f9xk2mq7plr8stz",
1563
2581
  method: "PATCH",
1564
2582
  path: "/asset/{assetKey}/accounting",
1565
2583
  pathParameters: [{
@@ -1568,7 +2586,132 @@ const surfaceCommands = [
1568
2586
  required: true
1569
2587
  }],
1570
2588
  queryFlags: [],
1571
- takesBody: true
2589
+ bodyFlags: [
2590
+ {
2591
+ name: "allocated-indirect",
2592
+ jsonPath: ["allocatedIndirect"],
2593
+ description: "One unit's share of overhead, a cost component",
2594
+ valueName: "value",
2595
+ nullable: true,
2596
+ schema: z.string()
2597
+ },
2598
+ {
2599
+ name: "bill-of-materials",
2600
+ jsonPath: ["billOfMaterials"],
2601
+ description: "The parts cost of one unit, a cost component",
2602
+ valueName: "value",
2603
+ nullable: true,
2604
+ schema: z.string()
2605
+ },
2606
+ {
2607
+ name: "depreciation-model",
2608
+ jsonPath: ["depreciationModel"],
2609
+ description: "The method one unit is depreciated by, or null to clear it",
2610
+ valueName: "value",
2611
+ schema: toEnum([
2612
+ "DOUBLE_DECLINING",
2613
+ "STRAIGHT_LINE",
2614
+ "SUM_YEAR",
2615
+ "UNIT_OF_PRODUCTION"
2616
+ ])
2617
+ },
2618
+ {
2619
+ name: "direct-labor",
2620
+ jsonPath: ["directLabor"],
2621
+ description: "The labor cost to build one unit, a cost component",
2622
+ valueName: "value",
2623
+ nullable: true,
2624
+ schema: z.string()
2625
+ },
2626
+ {
2627
+ name: "freight-inbound",
2628
+ jsonPath: ["freightInbound"],
2629
+ description: "The shipping cost to receive one unit, a cost component",
2630
+ valueName: "value",
2631
+ nullable: true,
2632
+ schema: z.string()
2633
+ },
2634
+ {
2635
+ name: "freight-outbound",
2636
+ jsonPath: ["freightOutbound"],
2637
+ description: "The shipping cost to deploy one unit, a deployment cost component",
2638
+ valueName: "value",
2639
+ nullable: true,
2640
+ schema: z.string()
2641
+ },
2642
+ {
2643
+ name: "in-service-date",
2644
+ jsonPath: ["inServiceDate"],
2645
+ description: "The day the asset was put into service and began depreciating, or null to clear it",
2646
+ valueName: "value",
2647
+ nullable: true,
2648
+ schema: z.string()
2649
+ },
2650
+ {
2651
+ name: "installation",
2652
+ jsonPath: ["installation"],
2653
+ description: "The cost to install one unit, a deployment cost component",
2654
+ valueName: "value",
2655
+ nullable: true,
2656
+ schema: z.string()
2657
+ },
2658
+ {
2659
+ name: "interest",
2660
+ jsonPath: ["interest"],
2661
+ description: "The financing cost of one unit, a cost component",
2662
+ valueName: "value",
2663
+ nullable: true,
2664
+ schema: z.string()
2665
+ },
2666
+ {
2667
+ name: "is-in-service-date-managed-automatically",
2668
+ jsonPath: ["isInServiceDateManagedAutomatically"],
2669
+ description: "Whether Hardfin sets the in-service date itself, which sending an in-service date turns off",
2670
+ negatable: true,
2671
+ nullable: true,
2672
+ schema: z.boolean()
2673
+ },
2674
+ {
2675
+ name: "salvage-value",
2676
+ jsonPath: ["salvageValue"],
2677
+ description: "The value one unit keeps at the end of its useful life, or null to clear it",
2678
+ valueName: "value",
2679
+ nullable: true,
2680
+ schema: z.string()
2681
+ },
2682
+ {
2683
+ name: "simple-cost-basis",
2684
+ jsonPath: ["simpleCostBasis"],
2685
+ description: "A single cost for one unit, which cannot be sent together with the cost components",
2686
+ valueName: "value",
2687
+ nullable: true,
2688
+ schema: z.string()
2689
+ },
2690
+ {
2691
+ name: "tariffs",
2692
+ jsonPath: ["tariffs"],
2693
+ description: "The import duty paid on one unit, a cost component",
2694
+ valueName: "value",
2695
+ nullable: true,
2696
+ schema: z.string()
2697
+ },
2698
+ {
2699
+ name: "tax",
2700
+ jsonPath: ["tax"],
2701
+ description: "The tax paid on one unit, a cost component",
2702
+ valueName: "value",
2703
+ nullable: true,
2704
+ schema: z.string()
2705
+ },
2706
+ {
2707
+ name: "useful-life",
2708
+ jsonPath: ["usefulLife"],
2709
+ description: "The number of months one unit is depreciated over, or null to clear it",
2710
+ valueName: "number",
2711
+ nullable: true,
2712
+ schema: z.coerce.number().int()
2713
+ }
2714
+ ]
1572
2715
  }), {
1573
2716
  name: "in-service-management",
1574
2717
  summary: "In service management commands",
@@ -1578,6 +2721,7 @@ const surfaceCommands = [
1578
2721
  subcommands: [defineOperation({
1579
2722
  name: "update",
1580
2723
  summary: "Toggle in service date management",
2724
+ example: "hardfin asset accounting in-service-management update ast_4f9xk2mq7plr8stz --automatic <value>",
1581
2725
  method: "PATCH",
1582
2726
  path: "/asset/{assetKey}/accounting/in-service-management",
1583
2727
  pathParameters: [{
@@ -1586,7 +2730,14 @@ const surfaceCommands = [
1586
2730
  required: true
1587
2731
  }],
1588
2732
  queryFlags: [],
1589
- takesBody: true
2733
+ bodyFlags: [{
2734
+ name: "automatic",
2735
+ jsonPath: ["automatic"],
2736
+ description: "Whether Hardfin sets the asset's in-service date itself",
2737
+ negatable: true,
2738
+ required: true,
2739
+ schema: z.boolean()
2740
+ }]
1590
2741
  })]
1591
2742
  }]
1592
2743
  },
@@ -1599,6 +2750,7 @@ const surfaceCommands = [
1599
2750
  subcommands: [defineOperation({
1600
2751
  name: "create",
1601
2752
  summary: "Create asset cost adjustment",
2753
+ example: "hardfin asset cost-adjustment create ast_4f9xk2mq7plr8stz --adjustment-type <value> --amount <value> --effective-date <value>",
1602
2754
  method: "POST",
1603
2755
  path: "/asset/{assetKey}/cost-adjustment",
1604
2756
  pathParameters: [{
@@ -1607,7 +2759,58 @@ const surfaceCommands = [
1607
2759
  required: true
1608
2760
  }],
1609
2761
  queryFlags: [],
1610
- takesBody: true
2762
+ bodyFlags: [
2763
+ {
2764
+ name: "adjustment-type",
2765
+ jsonPath: ["adjustmentType"],
2766
+ description: "Whether the adjustment adds to the asset's cost basis or writes it down",
2767
+ valueName: "value",
2768
+ required: true,
2769
+ schema: toEnum(["CAPITALIZATION", "IMPAIRMENT"])
2770
+ },
2771
+ {
2772
+ name: "amount",
2773
+ jsonPath: ["amount"],
2774
+ description: "How much the adjustment changes the cost basis by, which must be greater than zero",
2775
+ valueName: "value",
2776
+ required: true,
2777
+ schema: z.string()
2778
+ },
2779
+ {
2780
+ name: "effective-date",
2781
+ jsonPath: ["effectiveDate"],
2782
+ description: "The day the adjustment takes effect, which cannot be in the future",
2783
+ valueName: "value",
2784
+ required: true,
2785
+ schema: z.string()
2786
+ },
2787
+ {
2788
+ name: "notes",
2789
+ jsonPath: ["notes"],
2790
+ description: "Free-form detail about the adjustment, which a reason of OTHER requires",
2791
+ valueName: "value",
2792
+ nullable: true,
2793
+ schema: z.string()
2794
+ },
2795
+ {
2796
+ name: "reason",
2797
+ jsonPath: ["reason"],
2798
+ description: "Why the adjustment was made, which must be one its adjustment type allows",
2799
+ valueName: "value",
2800
+ required: true,
2801
+ schema: toEnum([
2802
+ "ADDITION",
2803
+ "BETTERMENT",
2804
+ "DAMAGE",
2805
+ "INSTALLATION",
2806
+ "LIFE_EXTENSION",
2807
+ "MARKET_DECLINE",
2808
+ "OBSOLESCENCE",
2809
+ "OTHER",
2810
+ "REGULATORY"
2811
+ ])
2812
+ }
2813
+ ]
1611
2814
  })]
1612
2815
  },
1613
2816
  {
@@ -1619,6 +2822,7 @@ const surfaceCommands = [
1619
2822
  subcommands: [defineOperation({
1620
2823
  name: "list",
1621
2824
  summary: "Get asset event list",
2825
+ example: "hardfin asset event list ast_4f9xk2mq7plr8stz",
1622
2826
  method: "GET",
1623
2827
  path: "/asset/{assetKey}/event",
1624
2828
  pathParameters: [{
@@ -1627,7 +2831,7 @@ const surfaceCommands = [
1627
2831
  required: true
1628
2832
  }],
1629
2833
  queryFlags: [],
1630
- takesBody: false
2834
+ bodyFlags: []
1631
2835
  })]
1632
2836
  },
1633
2837
  {
@@ -1639,6 +2843,7 @@ const surfaceCommands = [
1639
2843
  subcommands: [defineOperation({
1640
2844
  name: "list",
1641
2845
  summary: "Get asset event group listing",
2846
+ example: "hardfin asset event-group list ast_4f9xk2mq7plr8stz",
1642
2847
  method: "GET",
1643
2848
  path: "/asset/{assetKey}/event-group",
1644
2849
  pathParameters: [{
@@ -1659,10 +2864,11 @@ const surfaceCommands = [
1659
2864
  valueName: "value",
1660
2865
  schema: z.string()
1661
2866
  }],
1662
- takesBody: false
2867
+ bodyFlags: []
1663
2868
  }), defineOperation({
1664
2869
  name: "get",
1665
2870
  summary: "Get asset event group",
2871
+ example: "hardfin asset event-group get ast_4f9xk2mq7plr8stz aeg_3mx8kq2plr7stz4w",
1666
2872
  method: "GET",
1667
2873
  path: "/asset/{assetKey}/event-group/{eventGroupKey}",
1668
2874
  pathParameters: [{
@@ -1675,7 +2881,7 @@ const surfaceCommands = [
1675
2881
  required: true
1676
2882
  }],
1677
2883
  queryFlags: [],
1678
- takesBody: false
2884
+ bodyFlags: []
1679
2885
  })]
1680
2886
  },
1681
2887
  {
@@ -1687,6 +2893,7 @@ const surfaceCommands = [
1687
2893
  subcommands: [defineOperation({
1688
2894
  name: "list",
1689
2895
  summary: "Get asset files",
2896
+ example: "hardfin asset file list ast_4f9xk2mq7plr8stz",
1690
2897
  method: "GET",
1691
2898
  path: "/asset/{assetKey}/file",
1692
2899
  pathParameters: [{
@@ -1695,10 +2902,11 @@ const surfaceCommands = [
1695
2902
  required: true
1696
2903
  }],
1697
2904
  queryFlags: [],
1698
- takesBody: false
2905
+ bodyFlags: []
1699
2906
  }), defineOperation({
1700
2907
  name: "delete",
1701
2908
  summary: "Delete asset file",
2909
+ example: "hardfin asset file delete ast_4f9xk2mq7plr8stz file_7hq2mx9pkr4stz8w",
1702
2910
  method: "DELETE",
1703
2911
  path: "/asset/{assetKey}/file/{fileKey}",
1704
2912
  pathParameters: [{
@@ -1711,7 +2919,7 @@ const surfaceCommands = [
1711
2919
  required: true
1712
2920
  }],
1713
2921
  queryFlags: [],
1714
- takesBody: false
2922
+ bodyFlags: []
1715
2923
  })]
1716
2924
  },
1717
2925
  {
@@ -1723,6 +2931,7 @@ const surfaceCommands = [
1723
2931
  subcommands: [defineOperation({
1724
2932
  name: "list",
1725
2933
  summary: "Get asset functional status history",
2934
+ example: "hardfin asset functional-status list ast_4f9xk2mq7plr8stz",
1726
2935
  method: "GET",
1727
2936
  path: "/asset/{assetKey}/functional-status",
1728
2937
  pathParameters: [{
@@ -1731,7 +2940,7 @@ const surfaceCommands = [
1731
2940
  required: true
1732
2941
  }],
1733
2942
  queryFlags: [],
1734
- takesBody: false
2943
+ bodyFlags: []
1735
2944
  })]
1736
2945
  },
1737
2946
  {
@@ -1744,6 +2953,7 @@ const surfaceCommands = [
1744
2953
  defineOperation({
1745
2954
  name: "list",
1746
2955
  summary: "Get asset ownership history",
2956
+ example: "hardfin asset ownership list ast_4f9xk2mq7plr8stz",
1747
2957
  method: "GET",
1748
2958
  path: "/asset/{assetKey}/ownership",
1749
2959
  pathParameters: [{
@@ -1752,11 +2962,12 @@ const surfaceCommands = [
1752
2962
  required: true
1753
2963
  }],
1754
2964
  queryFlags: [],
1755
- takesBody: false
2965
+ bodyFlags: []
1756
2966
  }),
1757
2967
  defineOperation({
1758
2968
  name: "create",
1759
2969
  summary: "Create asset ownership",
2970
+ example: "hardfin asset ownership create ast_4f9xk2mq7plr8stz --customer-id <value> --date <value>",
1760
2971
  method: "POST",
1761
2972
  path: "/asset/{assetKey}/ownership",
1762
2973
  pathParameters: [{
@@ -1765,11 +2976,37 @@ const surfaceCommands = [
1765
2976
  required: true
1766
2977
  }],
1767
2978
  queryFlags: [],
1768
- takesBody: true
2979
+ bodyFlags: [
2980
+ {
2981
+ name: "customer-id",
2982
+ jsonPath: ["customerId"],
2983
+ description: "The ID of the customer that takes ownership of the asset",
2984
+ valueName: "value",
2985
+ required: true,
2986
+ schema: z.string()
2987
+ },
2988
+ {
2989
+ name: "date",
2990
+ jsonPath: ["date"],
2991
+ description: "The day the customer takes ownership, which cannot be in the future",
2992
+ valueName: "value",
2993
+ required: true,
2994
+ schema: z.string()
2995
+ },
2996
+ {
2997
+ name: "sale-price",
2998
+ jsonPath: ["salePrice"],
2999
+ description: "What the customer paid for the asset, which cannot be negative",
3000
+ valueName: "value",
3001
+ nullable: true,
3002
+ schema: z.string()
3003
+ }
3004
+ ]
1769
3005
  }),
1770
3006
  defineOperation({
1771
3007
  name: "clear",
1772
3008
  summary: "Clear the ownership an asset holds today",
3009
+ example: "hardfin asset ownership clear ast_4f9xk2mq7plr8stz --date <value>",
1773
3010
  method: "DELETE",
1774
3011
  path: "/asset/{assetKey}/ownership",
1775
3012
  pathParameters: [{
@@ -1778,11 +3015,19 @@ const surfaceCommands = [
1778
3015
  required: true
1779
3016
  }],
1780
3017
  queryFlags: [],
1781
- takesBody: true
3018
+ bodyFlags: [{
3019
+ name: "date",
3020
+ jsonPath: ["date"],
3021
+ description: "The day your organization takes the asset back, which cannot be in the future",
3022
+ valueName: "value",
3023
+ required: true,
3024
+ schema: z.string()
3025
+ }]
1782
3026
  }),
1783
3027
  defineOperation({
1784
3028
  name: "get",
1785
3029
  summary: "Get asset ownership segment",
3030
+ example: "hardfin asset ownership get ast_4f9xk2mq7plr8stz aown_8kq2mx7plr4stz9w",
1786
3031
  method: "GET",
1787
3032
  path: "/asset/{assetKey}/ownership/{segmentKey}",
1788
3033
  pathParameters: [{
@@ -1795,11 +3040,12 @@ const surfaceCommands = [
1795
3040
  required: true
1796
3041
  }],
1797
3042
  queryFlags: [],
1798
- takesBody: false
3043
+ bodyFlags: []
1799
3044
  }),
1800
3045
  defineOperation({
1801
3046
  name: "update",
1802
3047
  summary: "Patch asset ownership segment",
3048
+ example: "hardfin asset ownership update ast_4f9xk2mq7plr8stz aown_8kq2mx7plr4stz9w",
1803
3049
  method: "PATCH",
1804
3050
  path: "/asset/{assetKey}/ownership/{segmentKey}",
1805
3051
  pathParameters: [{
@@ -1812,11 +3058,37 @@ const surfaceCommands = [
1812
3058
  required: true
1813
3059
  }],
1814
3060
  queryFlags: [],
1815
- takesBody: true
3061
+ bodyFlags: [
3062
+ {
3063
+ name: "customer-id",
3064
+ jsonPath: ["customerId"],
3065
+ description: "The ID of the customer that owned the asset during the segment",
3066
+ valueName: "value",
3067
+ nullable: true,
3068
+ schema: z.string()
3069
+ },
3070
+ {
3071
+ name: "date",
3072
+ jsonPath: ["date"],
3073
+ description: "The day the segment's owner took ownership, which cannot be in the future",
3074
+ valueName: "value",
3075
+ nullable: true,
3076
+ schema: z.string()
3077
+ },
3078
+ {
3079
+ name: "sale-price",
3080
+ jsonPath: ["salePrice"],
3081
+ description: "What the owner paid for the asset, which cannot be negative",
3082
+ valueName: "value",
3083
+ nullable: true,
3084
+ schema: z.string()
3085
+ }
3086
+ ]
1816
3087
  }),
1817
3088
  defineOperation({
1818
3089
  name: "delete",
1819
3090
  summary: "Delete asset ownership segment",
3091
+ example: "hardfin asset ownership delete ast_4f9xk2mq7plr8stz aown_8kq2mx7plr4stz9w",
1820
3092
  method: "DELETE",
1821
3093
  path: "/asset/{assetKey}/ownership/{segmentKey}",
1822
3094
  pathParameters: [{
@@ -1829,7 +3101,7 @@ const surfaceCommands = [
1829
3101
  required: true
1830
3102
  }],
1831
3103
  queryFlags: [],
1832
- takesBody: false
3104
+ bodyFlags: []
1833
3105
  })
1834
3106
  ]
1835
3107
  },
@@ -1842,6 +3114,7 @@ const surfaceCommands = [
1842
3114
  subcommands: [defineOperation({
1843
3115
  name: "create",
1844
3116
  summary: "Scrap asset",
3117
+ example: "hardfin asset scrap create ast_4f9xk2mq7plr8stz --disposal-date <value>",
1845
3118
  method: "POST",
1846
3119
  path: "/asset/{assetKey}/scrap",
1847
3120
  pathParameters: [{
@@ -1850,7 +3123,32 @@ const surfaceCommands = [
1850
3123
  required: true
1851
3124
  }],
1852
3125
  queryFlags: [],
1853
- takesBody: true
3126
+ bodyFlags: [
3127
+ {
3128
+ name: "disposal-date",
3129
+ jsonPath: ["disposalDate"],
3130
+ description: "The day the asset was scrapped",
3131
+ valueName: "value",
3132
+ required: true,
3133
+ schema: z.string()
3134
+ },
3135
+ {
3136
+ name: "disposal-price",
3137
+ jsonPath: ["disposalPrice"],
3138
+ description: "What the scrapped asset was sold for, or null when it was not sold",
3139
+ valueName: "value",
3140
+ nullable: true,
3141
+ schema: z.string()
3142
+ },
3143
+ {
3144
+ name: "disposal-reason",
3145
+ jsonPath: ["disposalReason"],
3146
+ description: "Why the asset was scrapped",
3147
+ valueName: "value",
3148
+ nullable: true,
3149
+ schema: z.string()
3150
+ }
3151
+ ]
1854
3152
  })]
1855
3153
  },
1856
3154
  {
@@ -1862,6 +3160,7 @@ const surfaceCommands = [
1862
3160
  subcommands: [defineOperation({
1863
3161
  name: "create",
1864
3162
  summary: "Unscrap asset",
3163
+ example: "hardfin asset unscrap create ast_4f9xk2mq7plr8stz",
1865
3164
  method: "POST",
1866
3165
  path: "/asset/{assetKey}/unscrap",
1867
3166
  pathParameters: [{
@@ -1870,7 +3169,7 @@ const surfaceCommands = [
1870
3169
  required: true
1871
3170
  }],
1872
3171
  queryFlags: [],
1873
- takesBody: false
3172
+ bodyFlags: []
1874
3173
  })]
1875
3174
  },
1876
3175
  {
@@ -1882,6 +3181,7 @@ const surfaceCommands = [
1882
3181
  subcommands: [defineOperation({
1883
3182
  name: "list",
1884
3183
  summary: "Get asset URL links",
3184
+ example: "hardfin asset url-link list ast_4f9xk2mq7plr8stz",
1885
3185
  method: "GET",
1886
3186
  path: "/asset/{assetKey}/url-link",
1887
3187
  pathParameters: [{
@@ -1890,10 +3190,11 @@ const surfaceCommands = [
1890
3190
  required: true
1891
3191
  }],
1892
3192
  queryFlags: [],
1893
- takesBody: false
3193
+ bodyFlags: []
1894
3194
  }), defineOperation({
1895
3195
  name: "create",
1896
3196
  summary: "Create asset URL link",
3197
+ example: "hardfin asset url-link create ast_4f9xk2mq7plr8stz --url <value>",
1897
3198
  method: "POST",
1898
3199
  path: "/asset/{assetKey}/url-link",
1899
3200
  pathParameters: [{
@@ -1902,7 +3203,21 @@ const surfaceCommands = [
1902
3203
  required: true
1903
3204
  }],
1904
3205
  queryFlags: [],
1905
- takesBody: true
3206
+ bodyFlags: [{
3207
+ name: "name",
3208
+ jsonPath: ["name"],
3209
+ description: "The link's display name, or null to show the address instead",
3210
+ valueName: "value",
3211
+ nullable: true,
3212
+ schema: z.string()
3213
+ }, {
3214
+ name: "url",
3215
+ jsonPath: ["url"],
3216
+ description: "The address the link points to",
3217
+ valueName: "value",
3218
+ required: true,
3219
+ schema: z.string()
3220
+ }]
1906
3221
  })]
1907
3222
  },
1908
3223
  {
@@ -1914,6 +3229,7 @@ const surfaceCommands = [
1914
3229
  subcommands: [defineOperation({
1915
3230
  name: "create",
1916
3231
  summary: "Create asset useful life revision",
3232
+ example: "hardfin asset useful-life-revision create ast_4f9xk2mq7plr8stz --effective-date <value> --reason <value> --useful-life-months <number>",
1917
3233
  method: "POST",
1918
3234
  path: "/asset/{assetKey}/useful-life-revision",
1919
3235
  pathParameters: [{
@@ -1922,7 +3238,48 @@ const surfaceCommands = [
1922
3238
  required: true
1923
3239
  }],
1924
3240
  queryFlags: [],
1925
- takesBody: true
3241
+ bodyFlags: [
3242
+ {
3243
+ name: "effective-date",
3244
+ jsonPath: ["effectiveDate"],
3245
+ description: "The day the revised useful life takes effect, which cannot be in the future",
3246
+ valueName: "value",
3247
+ required: true,
3248
+ schema: z.string()
3249
+ },
3250
+ {
3251
+ name: "notes",
3252
+ jsonPath: ["notes"],
3253
+ description: "Free-form detail about the revision, which a reason of OTHER requires",
3254
+ valueName: "value",
3255
+ nullable: true,
3256
+ schema: z.string()
3257
+ },
3258
+ {
3259
+ name: "reason",
3260
+ jsonPath: ["reason"],
3261
+ description: "Why the useful life was revised",
3262
+ valueName: "value",
3263
+ required: true,
3264
+ schema: toEnum([
3265
+ "CHANGE_IN_USE",
3266
+ "DAMAGE",
3267
+ "OBSOLESCENCE",
3268
+ "OTHER",
3269
+ "REASSESSMENT",
3270
+ "REFURBISHMENT",
3271
+ "REGULATORY"
3272
+ ])
3273
+ },
3274
+ {
3275
+ name: "useful-life-months",
3276
+ jsonPath: ["usefulLifeMonths"],
3277
+ description: "The asset's revised useful life in months",
3278
+ valueName: "number",
3279
+ required: true,
3280
+ schema: z.coerce.number().int()
3281
+ }
3282
+ ]
1926
3283
  })]
1927
3284
  }
1928
3285
  ]
@@ -1937,6 +3294,7 @@ const surfaceCommands = [
1937
3294
  defineOperation({
1938
3295
  name: "list",
1939
3296
  summary: "Get customers",
3297
+ example: "hardfin customer list --limit 10",
1940
3298
  method: "GET",
1941
3299
  path: "/customer",
1942
3300
  pathParameters: [],
@@ -1946,14 +3304,14 @@ const surfaceCommands = [
1946
3304
  queryName: "page",
1947
3305
  description: "The page to return, starting at 1",
1948
3306
  valueName: "number",
1949
- schema: z.coerce.number()
3307
+ schema: z.coerce.number().int()
1950
3308
  },
1951
3309
  {
1952
3310
  name: "limit",
1953
3311
  queryName: "limit",
1954
3312
  description: "The number of records per page, from 1 to 100",
1955
3313
  valueName: "number",
1956
- schema: z.coerce.number()
3314
+ schema: z.coerce.number().int().min(1).max(100)
1957
3315
  },
1958
3316
  {
1959
3317
  name: "sort-by",
@@ -1967,14 +3325,14 @@ const surfaceCommands = [
1967
3325
  queryName: "sortOrder",
1968
3326
  description: "The sort direction",
1969
3327
  valueName: "value",
1970
- schema: z.enum(["ASC", "DESC"])
3328
+ schema: toEnum(["ASC", "DESC"])
1971
3329
  },
1972
3330
  {
1973
3331
  name: "archived",
1974
3332
  queryName: "archived",
1975
3333
  description: "Whether to return unarchived records, archived records, or all of them",
1976
3334
  valueName: "value",
1977
- schema: z.enum([
3335
+ schema: toEnum([
1978
3336
  "all",
1979
3337
  "false",
1980
3338
  "true"
@@ -2008,62 +3366,262 @@ const surfaceCommands = [
2008
3366
  schema: z.string()
2009
3367
  }
2010
3368
  ],
2011
- takesBody: false
3369
+ bodyFlags: []
2012
3370
  }),
2013
3371
  defineOperation({
2014
3372
  name: "create",
2015
3373
  summary: "Create customer",
3374
+ example: "hardfin customer create --name <value>",
2016
3375
  method: "POST",
2017
3376
  path: "/customer",
2018
3377
  pathParameters: [],
2019
3378
  queryFlags: [],
2020
- takesBody: true
2021
- }),
2022
- defineOperation({
2023
- name: "get",
2024
- summary: "Get customer",
2025
- method: "GET",
2026
- path: "/customer/{customerKey}",
2027
- pathParameters: [{
2028
- name: "customerKey",
2029
- description: "The customer's key",
2030
- required: true
2031
- }],
2032
- queryFlags: [],
2033
- takesBody: false
2034
- }),
2035
- defineOperation({
2036
- name: "update",
2037
- summary: "Patch customer",
2038
- method: "PATCH",
2039
- path: "/customer/{customerKey}",
2040
- pathParameters: [{
2041
- name: "customerKey",
2042
- description: "The customer's key",
2043
- required: true
2044
- }],
2045
- queryFlags: [],
2046
- takesBody: true
2047
- })
2048
- ]
2049
- },
2050
- {
2051
- name: "file",
2052
- summary: "File commands",
2053
- arguments: [],
2054
- flags: [],
2055
- examples: [],
2056
- subcommands: [defineOperation({
2057
- name: "create",
2058
- summary: "Upload file",
3379
+ bodyFlags: [
3380
+ {
3381
+ name: "billing-address",
3382
+ jsonPath: ["billingAddress"],
3383
+ description: "The address invoices are sent to",
3384
+ valueName: "value",
3385
+ nullable: true,
3386
+ schema: z.string()
3387
+ },
3388
+ {
3389
+ name: "billing-contact-email",
3390
+ jsonPath: ["billingContact", "email"],
3391
+ description: "The billing contact's email address",
3392
+ valueName: "value",
3393
+ nullable: true,
3394
+ schema: z.string()
3395
+ },
3396
+ {
3397
+ name: "billing-contact-name",
3398
+ jsonPath: ["billingContact", "name"],
3399
+ description: "The billing contact's name",
3400
+ valueName: "value",
3401
+ nullable: true,
3402
+ schema: z.string()
3403
+ },
3404
+ {
3405
+ name: "billing-contact-phone",
3406
+ jsonPath: ["billingContact", "phone"],
3407
+ description: "The billing contact's phone number",
3408
+ valueName: "value",
3409
+ nullable: true,
3410
+ schema: z.string()
3411
+ },
3412
+ {
3413
+ name: "comment",
3414
+ jsonPath: ["comment"],
3415
+ description: "A free-form note about the customer",
3416
+ valueName: "value",
3417
+ nullable: true,
3418
+ schema: z.string()
3419
+ },
3420
+ {
3421
+ name: "domain",
3422
+ jsonPath: ["domain"],
3423
+ description: "The customer's web domain, used to look up its logo",
3424
+ valueName: "value",
3425
+ nullable: true,
3426
+ schema: z.string()
3427
+ },
3428
+ {
3429
+ name: "external-id",
3430
+ jsonPath: ["externalId"],
3431
+ description: "The customer's identifier in another system",
3432
+ valueName: "value",
3433
+ nullable: true,
3434
+ schema: z.string()
3435
+ },
3436
+ {
3437
+ name: "is-customer",
3438
+ jsonPath: ["isCustomer"],
3439
+ description: "Whether the company is a customer",
3440
+ negatable: true,
3441
+ schema: z.boolean()
3442
+ },
3443
+ {
3444
+ name: "is-supplier",
3445
+ jsonPath: ["isSupplier"],
3446
+ description: "Whether the company is a supplier",
3447
+ negatable: true,
3448
+ schema: z.boolean()
3449
+ },
3450
+ {
3451
+ name: "name",
3452
+ jsonPath: ["name"],
3453
+ description: "The customer's display name",
3454
+ valueName: "value",
3455
+ required: true,
3456
+ schema: z.string()
3457
+ }
3458
+ ]
3459
+ }),
3460
+ defineOperation({
3461
+ name: "get",
3462
+ summary: "Get customer",
3463
+ example: "hardfin customer get cust_V1StGXR8Z5jdHi6B",
3464
+ method: "GET",
3465
+ path: "/customer/{customerKey}",
3466
+ pathParameters: [{
3467
+ name: "customerKey",
3468
+ description: "The customer's key",
3469
+ required: true
3470
+ }],
3471
+ queryFlags: [],
3472
+ bodyFlags: []
3473
+ }),
3474
+ defineOperation({
3475
+ name: "update",
3476
+ summary: "Patch customer",
3477
+ example: "hardfin customer update cust_V1StGXR8Z5jdHi6B",
3478
+ method: "PATCH",
3479
+ path: "/customer/{customerKey}",
3480
+ pathParameters: [{
3481
+ name: "customerKey",
3482
+ description: "The customer's key",
3483
+ required: true
3484
+ }],
3485
+ queryFlags: [],
3486
+ bodyFlags: [
3487
+ {
3488
+ name: "billing-address",
3489
+ jsonPath: ["billingAddress"],
3490
+ description: "The address invoices are sent to",
3491
+ valueName: "value",
3492
+ nullable: true,
3493
+ schema: z.string()
3494
+ },
3495
+ {
3496
+ name: "billing-contact-email",
3497
+ jsonPath: ["billingContact", "email"],
3498
+ description: "The billing contact's email address",
3499
+ valueName: "value",
3500
+ nullable: true,
3501
+ schema: z.string()
3502
+ },
3503
+ {
3504
+ name: "billing-contact-name",
3505
+ jsonPath: ["billingContact", "name"],
3506
+ description: "The billing contact's name",
3507
+ valueName: "value",
3508
+ nullable: true,
3509
+ schema: z.string()
3510
+ },
3511
+ {
3512
+ name: "billing-contact-phone",
3513
+ jsonPath: ["billingContact", "phone"],
3514
+ description: "The billing contact's phone number",
3515
+ valueName: "value",
3516
+ nullable: true,
3517
+ schema: z.string()
3518
+ },
3519
+ {
3520
+ name: "comment",
3521
+ jsonPath: ["comment"],
3522
+ description: "A free-form note about the customer",
3523
+ valueName: "value",
3524
+ nullable: true,
3525
+ schema: z.string()
3526
+ },
3527
+ {
3528
+ name: "domain",
3529
+ jsonPath: ["domain"],
3530
+ description: "The customer's web domain, used to look up its logo",
3531
+ valueName: "value",
3532
+ nullable: true,
3533
+ schema: z.string()
3534
+ },
3535
+ {
3536
+ name: "external-id",
3537
+ jsonPath: ["externalId"],
3538
+ description: "The customer's identifier in another system",
3539
+ valueName: "value",
3540
+ nullable: true,
3541
+ schema: z.string()
3542
+ },
3543
+ {
3544
+ name: "is-archived",
3545
+ jsonPath: ["isArchived"],
3546
+ description: "Whether the customer is archived",
3547
+ negatable: true,
3548
+ nullable: true,
3549
+ schema: z.boolean()
3550
+ },
3551
+ {
3552
+ name: "is-customer",
3553
+ jsonPath: ["isCustomer"],
3554
+ description: "Whether the company is a customer",
3555
+ negatable: true,
3556
+ nullable: true,
3557
+ schema: z.boolean()
3558
+ },
3559
+ {
3560
+ name: "is-supplier",
3561
+ jsonPath: ["isSupplier"],
3562
+ description: "Whether the company is a supplier",
3563
+ negatable: true,
3564
+ nullable: true,
3565
+ schema: z.boolean()
3566
+ },
3567
+ {
3568
+ name: "name",
3569
+ jsonPath: ["name"],
3570
+ description: "The customer's display name",
3571
+ valueName: "value",
3572
+ nullable: true,
3573
+ schema: z.string()
3574
+ }
3575
+ ]
3576
+ })
3577
+ ]
3578
+ },
3579
+ {
3580
+ name: "file",
3581
+ summary: "File commands",
3582
+ arguments: [],
3583
+ flags: [],
3584
+ examples: [],
3585
+ subcommands: [defineOperation({
3586
+ name: "create",
3587
+ summary: "Upload file",
3588
+ example: "hardfin file create --file-type <value> --for-entity <value> --file photo.jpg",
2059
3589
  method: "POST",
2060
3590
  path: "/file",
2061
3591
  pathParameters: [],
2062
3592
  queryFlags: [],
2063
- takesBody: true
3593
+ bodyFlags: [],
3594
+ upload: {
3595
+ filePart: "data",
3596
+ fields: [
3597
+ {
3598
+ name: "file-type",
3599
+ jsonPath: ["fileType"],
3600
+ description: "The kind of file uploaded, which is ASSET_FILE, the only kind the API uploads",
3601
+ valueName: "value",
3602
+ required: true,
3603
+ schema: z.string()
3604
+ },
3605
+ {
3606
+ name: "for-entity",
3607
+ jsonPath: ["forEntity"],
3608
+ description: "The ID of the asset the file is attached to",
3609
+ valueName: "value",
3610
+ required: true,
3611
+ schema: z.string()
3612
+ },
3613
+ {
3614
+ name: "is-public",
3615
+ jsonPath: ["isPublic"],
3616
+ description: "Whether any organization's API key may download the file, which is false unless sent as true",
3617
+ schema: z.boolean()
3618
+ }
3619
+ ]
3620
+ }
2064
3621
  }), defineOperation({
2065
3622
  name: "get",
2066
3623
  summary: "Get file",
3624
+ example: "hardfin file get file_7hq2mx9pkr4stz8w",
2067
3625
  method: "GET",
2068
3626
  path: "/file/{fileKey}",
2069
3627
  pathParameters: [{
@@ -2077,7 +3635,8 @@ const surfaceCommands = [
2077
3635
  description: "True when the file downloads as an attachment rather than opening inline",
2078
3636
  schema: z.boolean()
2079
3637
  }],
2080
- takesBody: false
3638
+ bodyFlags: [],
3639
+ downloads: true
2081
3640
  })]
2082
3641
  },
2083
3642
  {
@@ -2090,6 +3649,7 @@ const surfaceCommands = [
2090
3649
  defineOperation({
2091
3650
  name: "list",
2092
3651
  summary: "Get items",
3652
+ example: "hardfin item list --limit 10",
2093
3653
  method: "GET",
2094
3654
  path: "/item",
2095
3655
  pathParameters: [],
@@ -2099,7 +3659,7 @@ const surfaceCommands = [
2099
3659
  queryName: "type",
2100
3660
  description: "The item type to list, SERVICE, DEVICE, or BULK, or absent for every type",
2101
3661
  valueName: "value",
2102
- schema: z.enum([
3662
+ schema: toEnum([
2103
3663
  "BULK",
2104
3664
  "DEVICE",
2105
3665
  "SERVICE"
@@ -2117,21 +3677,21 @@ const surfaceCommands = [
2117
3677
  queryName: "page",
2118
3678
  description: "The page to return, starting at 1",
2119
3679
  valueName: "number",
2120
- schema: z.coerce.number()
3680
+ schema: z.coerce.number().int()
2121
3681
  },
2122
3682
  {
2123
3683
  name: "limit",
2124
3684
  queryName: "limit",
2125
3685
  description: "The number of records per page, from 1 to 100",
2126
3686
  valueName: "number",
2127
- schema: z.coerce.number()
3687
+ schema: z.coerce.number().int().min(1).max(100)
2128
3688
  },
2129
3689
  {
2130
3690
  name: "sort-by",
2131
3691
  queryName: "sortBy",
2132
3692
  description: "The field to sort by",
2133
3693
  valueName: "value",
2134
- schema: z.enum([
3694
+ schema: toEnum([
2135
3695
  "name",
2136
3696
  "sku",
2137
3697
  "type",
@@ -2143,14 +3703,14 @@ const surfaceCommands = [
2143
3703
  queryName: "sortOrder",
2144
3704
  description: "The sort direction",
2145
3705
  valueName: "value",
2146
- schema: z.enum(["ASC", "DESC"])
3706
+ schema: toEnum(["ASC", "DESC"])
2147
3707
  },
2148
3708
  {
2149
3709
  name: "archived",
2150
3710
  queryName: "archived",
2151
3711
  description: "Whether to return unarchived records, archived records, or all of them",
2152
3712
  valueName: "value",
2153
- schema: z.enum([
3713
+ schema: toEnum([
2154
3714
  "all",
2155
3715
  "false",
2156
3716
  "true"
@@ -2164,20 +3724,131 @@ const surfaceCommands = [
2164
3724
  schema: z.string()
2165
3725
  }
2166
3726
  ],
2167
- takesBody: false
3727
+ bodyFlags: []
2168
3728
  }),
2169
3729
  defineOperation({
2170
3730
  name: "create",
2171
3731
  summary: "Create item",
3732
+ example: "hardfin item create --name <value> --sku <value> --type <value>",
2172
3733
  method: "POST",
2173
3734
  path: "/item",
2174
3735
  pathParameters: [],
2175
3736
  queryFlags: [],
2176
- takesBody: true
3737
+ bodyFlags: [
3738
+ {
3739
+ name: "accepts-bulk-serials",
3740
+ jsonPath: ["acceptsBulkSerials"],
3741
+ description: "Whether a BULK item records serial numbers on its units, which SERVICE and DEVICE items ignore",
3742
+ negatable: true,
3743
+ schema: z.boolean()
3744
+ },
3745
+ {
3746
+ name: "description",
3747
+ jsonPath: ["description"],
3748
+ description: "A free-form description of the item",
3749
+ valueName: "value",
3750
+ nullable: true,
3751
+ schema: z.string()
3752
+ },
3753
+ {
3754
+ name: "name",
3755
+ jsonPath: ["name"],
3756
+ description: "The item's display name",
3757
+ valueName: "value",
3758
+ required: true,
3759
+ schema: z.string()
3760
+ },
3761
+ {
3762
+ name: "sku",
3763
+ jsonPath: ["sku"],
3764
+ description: "The item's stock keeping unit, unique within your organization",
3765
+ valueName: "value",
3766
+ required: true,
3767
+ schema: z.string()
3768
+ },
3769
+ {
3770
+ name: "type",
3771
+ jsonPath: ["type"],
3772
+ description: "SERVICE for a non-physical item, DEVICE for a physical item tracked by serial number, or BULK for a part tracked by quantity",
3773
+ valueName: "value",
3774
+ required: true,
3775
+ schema: toEnum([
3776
+ "BULK",
3777
+ "DEVICE",
3778
+ "SERVICE"
3779
+ ])
3780
+ },
3781
+ {
3782
+ name: "unit-of-measure",
3783
+ jsonPath: ["unitOfMeasure"],
3784
+ description: "The unit a BULK item's quantities are counted in, which SERVICE and DEVICE items ignore",
3785
+ valueName: "value",
3786
+ schema: toEnum([
3787
+ "BG",
3788
+ "BO",
3789
+ "BX",
3790
+ "C62",
3791
+ "CMK",
3792
+ "CMT",
3793
+ "CR",
3794
+ "CS",
3795
+ "CT",
3796
+ "DMQ",
3797
+ "DR",
3798
+ "DZN",
3799
+ "EA",
3800
+ "EN",
3801
+ "FOT",
3802
+ "FTK",
3803
+ "FTQ",
3804
+ "GLL",
3805
+ "GRM",
3806
+ "GRO",
3807
+ "H87",
3808
+ "INH",
3809
+ "INK",
3810
+ "INQ",
3811
+ "KG",
3812
+ "KGM",
3813
+ "KMT",
3814
+ "KT",
3815
+ "LBR",
3816
+ "LO",
3817
+ "LTR",
3818
+ "MGM",
3819
+ "MLT",
3820
+ "MMK",
3821
+ "MMT",
3822
+ "MTK",
3823
+ "MTQ",
3824
+ "MTR",
3825
+ "ONZ",
3826
+ "OZA",
3827
+ "PK",
3828
+ "PR",
3829
+ "PTI",
3830
+ "PX",
3831
+ "QTI",
3832
+ "RL",
3833
+ "RO",
3834
+ "SET",
3835
+ "SMI",
3836
+ "ST",
3837
+ "STN",
3838
+ "SV",
3839
+ "TNE",
3840
+ "TU",
3841
+ "YDK",
3842
+ "YDQ",
3843
+ "YRD"
3844
+ ])
3845
+ }
3846
+ ]
2177
3847
  }),
2178
3848
  defineOperation({
2179
3849
  name: "get",
2180
3850
  summary: "Get item",
3851
+ example: "hardfin item get item_7Hq2Lm9XcR4tWz8K",
2181
3852
  method: "GET",
2182
3853
  path: "/item/{itemKey}",
2183
3854
  pathParameters: [{
@@ -2186,11 +3857,12 @@ const surfaceCommands = [
2186
3857
  required: true
2187
3858
  }],
2188
3859
  queryFlags: [],
2189
- takesBody: false
3860
+ bodyFlags: []
2190
3861
  }),
2191
3862
  defineOperation({
2192
3863
  name: "update",
2193
3864
  summary: "Update item",
3865
+ example: "hardfin item update item_7Hq2Lm9XcR4tWz8K",
2194
3866
  method: "PATCH",
2195
3867
  path: "/item/{itemKey}",
2196
3868
  pathParameters: [{
@@ -2199,7 +3871,73 @@ const surfaceCommands = [
2199
3871
  required: true
2200
3872
  }],
2201
3873
  queryFlags: [],
2202
- takesBody: true
3874
+ bodyFlags: [
3875
+ {
3876
+ name: "accepts-bulk-serials",
3877
+ jsonPath: ["acceptsBulkSerials"],
3878
+ description: "Whether a BULK item records serial numbers on its units, read only beside type",
3879
+ negatable: true,
3880
+ nullable: true,
3881
+ schema: z.boolean()
3882
+ },
3883
+ {
3884
+ name: "description",
3885
+ jsonPath: ["description"],
3886
+ description: "The item's new description, or null to clear it",
3887
+ valueName: "value",
3888
+ nullable: true,
3889
+ schema: z.string()
3890
+ },
3891
+ {
3892
+ name: "field",
3893
+ jsonPath: ["fields"],
3894
+ description: "New positions for a DEVICE item's fields",
3895
+ valueName: "fieldId=,order=",
3896
+ repeatable: true,
3897
+ element: [
3898
+ "fieldId",
3899
+ "order",
3900
+ "section"
3901
+ ],
3902
+ schema: z.array(z.string())
3903
+ },
3904
+ {
3905
+ name: "is-archived",
3906
+ jsonPath: ["isArchived"],
3907
+ description: "Whether the item is archived, which cannot be null",
3908
+ negatable: true,
3909
+ nullable: true,
3910
+ schema: z.boolean()
3911
+ },
3912
+ {
3913
+ name: "name",
3914
+ jsonPath: ["name"],
3915
+ description: "The item's new display name, which cannot be empty",
3916
+ valueName: "value",
3917
+ nullable: true,
3918
+ schema: z.string()
3919
+ },
3920
+ {
3921
+ name: "sku",
3922
+ jsonPath: ["sku"],
3923
+ description: "The item's new stock keeping unit, which cannot be empty and must be unique within your organization",
3924
+ valueName: "value",
3925
+ nullable: true,
3926
+ schema: z.string()
3927
+ },
3928
+ {
3929
+ name: "type",
3930
+ jsonPath: ["type"],
3931
+ description: "The type to convert the item to, when the item's assets and inventory history allow the conversion",
3932
+ valueName: "value",
3933
+ nullable: true,
3934
+ schema: toEnum([
3935
+ "BULK",
3936
+ "DEVICE",
3937
+ "SERVICE"
3938
+ ])
3939
+ }
3940
+ ]
2203
3941
  }),
2204
3942
  {
2205
3943
  name: "accounting",
@@ -2210,6 +3948,7 @@ const surfaceCommands = [
2210
3948
  subcommands: [defineOperation({
2211
3949
  name: "update",
2212
3950
  summary: "Update item accounting",
3951
+ example: "hardfin item accounting update item_7Hq2Lm9XcR4tWz8K",
2213
3952
  method: "PATCH",
2214
3953
  path: "/item/{itemKey}/accounting",
2215
3954
  pathParameters: [{
@@ -2218,7 +3957,116 @@ const surfaceCommands = [
2218
3957
  required: true
2219
3958
  }],
2220
3959
  queryFlags: [],
2221
- takesBody: true
3960
+ bodyFlags: [
3961
+ {
3962
+ name: "allocated-indirect",
3963
+ jsonPath: ["allocatedIndirect"],
3964
+ description: "One unit's share of overhead, a cost component",
3965
+ valueName: "value",
3966
+ nullable: true,
3967
+ schema: z.string()
3968
+ },
3969
+ {
3970
+ name: "bill-of-materials",
3971
+ jsonPath: ["billOfMaterials"],
3972
+ description: "The parts cost of one unit, a cost component",
3973
+ valueName: "value",
3974
+ nullable: true,
3975
+ schema: z.string()
3976
+ },
3977
+ {
3978
+ name: "depreciation-model",
3979
+ jsonPath: ["depreciationModel"],
3980
+ description: "The method one unit is depreciated by, or null to clear it",
3981
+ valueName: "value",
3982
+ schema: toEnum([
3983
+ "DOUBLE_DECLINING",
3984
+ "STRAIGHT_LINE",
3985
+ "SUM_YEAR",
3986
+ "UNIT_OF_PRODUCTION"
3987
+ ])
3988
+ },
3989
+ {
3990
+ name: "direct-labor",
3991
+ jsonPath: ["directLabor"],
3992
+ description: "The labor cost to build one unit, a cost component",
3993
+ valueName: "value",
3994
+ nullable: true,
3995
+ schema: z.string()
3996
+ },
3997
+ {
3998
+ name: "freight-inbound",
3999
+ jsonPath: ["freightInbound"],
4000
+ description: "The shipping cost to receive one unit, a cost component",
4001
+ valueName: "value",
4002
+ nullable: true,
4003
+ schema: z.string()
4004
+ },
4005
+ {
4006
+ name: "freight-outbound",
4007
+ jsonPath: ["freightOutbound"],
4008
+ description: "The shipping cost to deploy one unit, a deployment cost component",
4009
+ valueName: "value",
4010
+ nullable: true,
4011
+ schema: z.string()
4012
+ },
4013
+ {
4014
+ name: "installation",
4015
+ jsonPath: ["installation"],
4016
+ description: "The cost to install one unit, a deployment cost component",
4017
+ valueName: "value",
4018
+ nullable: true,
4019
+ schema: z.string()
4020
+ },
4021
+ {
4022
+ name: "interest",
4023
+ jsonPath: ["interest"],
4024
+ description: "The financing cost of one unit, a cost component",
4025
+ valueName: "value",
4026
+ nullable: true,
4027
+ schema: z.string()
4028
+ },
4029
+ {
4030
+ name: "salvage-value",
4031
+ jsonPath: ["salvageValue"],
4032
+ description: "The value one unit keeps at the end of its useful life, or null to clear it",
4033
+ valueName: "value",
4034
+ nullable: true,
4035
+ schema: z.string()
4036
+ },
4037
+ {
4038
+ name: "simple-cost-basis",
4039
+ jsonPath: ["simpleCostBasis"],
4040
+ description: "A single cost for one unit, which cannot be sent together with the cost components",
4041
+ valueName: "value",
4042
+ nullable: true,
4043
+ schema: z.string()
4044
+ },
4045
+ {
4046
+ name: "tariffs",
4047
+ jsonPath: ["tariffs"],
4048
+ description: "The import duty paid on one unit, a cost component",
4049
+ valueName: "value",
4050
+ nullable: true,
4051
+ schema: z.string()
4052
+ },
4053
+ {
4054
+ name: "tax",
4055
+ jsonPath: ["tax"],
4056
+ description: "The tax paid on one unit, a cost component",
4057
+ valueName: "value",
4058
+ nullable: true,
4059
+ schema: z.string()
4060
+ },
4061
+ {
4062
+ name: "useful-life",
4063
+ jsonPath: ["usefulLife"],
4064
+ description: "The number of months one unit is depreciated over, or null to clear it",
4065
+ valueName: "number",
4066
+ nullable: true,
4067
+ schema: z.coerce.number().int()
4068
+ }
4069
+ ]
2222
4070
  })]
2223
4071
  },
2224
4072
  {
@@ -2231,6 +4079,7 @@ const surfaceCommands = [
2231
4079
  defineOperation({
2232
4080
  name: "create",
2233
4081
  summary: "Create item field",
4082
+ example: "hardfin item field create item_7Hq2Lm9XcR4tWz8K --field-type <value> --label <value> --order <number>",
2234
4083
  method: "POST",
2235
4084
  path: "/item/{itemKey}/field",
2236
4085
  pathParameters: [{
@@ -2239,11 +4088,56 @@ const surfaceCommands = [
2239
4088
  required: true
2240
4089
  }],
2241
4090
  queryFlags: [],
2242
- takesBody: true
4091
+ bodyFlags: [
4092
+ {
4093
+ name: "field-type",
4094
+ jsonPath: ["fieldType"],
4095
+ description: "The kind of value the field holds",
4096
+ valueName: "value",
4097
+ required: true,
4098
+ schema: toEnum([
4099
+ "BOOLEAN",
4100
+ "DATE",
4101
+ "DATE_TIME",
4102
+ "INTEGER",
4103
+ "MULTILINE_TEXT",
4104
+ "NUMBER",
4105
+ "TEXT",
4106
+ "TIME"
4107
+ ])
4108
+ },
4109
+ {
4110
+ name: "label",
4111
+ jsonPath: ["label"],
4112
+ description: "The field's display name",
4113
+ valueName: "value",
4114
+ required: true,
4115
+ schema: z.string()
4116
+ },
4117
+ {
4118
+ name: "order",
4119
+ jsonPath: ["order"],
4120
+ description: "The field's position within its section, starting at 0",
4121
+ valueName: "number",
4122
+ required: true,
4123
+ nullable: true,
4124
+ schema: z.coerce.number().int()
4125
+ },
4126
+ {
4127
+ name: "section",
4128
+ jsonPath: ["section"],
4129
+ description: "The group the field is shown in, starting at 0",
4130
+ valueName: "number",
4131
+ required: true,
4132
+ nullable: true,
4133
+ schema: z.coerce.number().int()
4134
+ }
4135
+ ]
2243
4136
  }),
2244
4137
  defineOperation({
2245
4138
  name: "update",
2246
4139
  summary: "Update item field",
4140
+ example: "hardfin item field update item_7Hq2Lm9XcR4tWz8K pfield_2wn8kq4lxp7rtz3m",
2247
4141
  method: "PATCH",
2248
4142
  path: "/item/{itemKey}/field/{fieldKey}",
2249
4143
  pathParameters: [{
@@ -2256,11 +4150,19 @@ const surfaceCommands = [
2256
4150
  required: true
2257
4151
  }],
2258
4152
  queryFlags: [],
2259
- takesBody: true
4153
+ bodyFlags: [{
4154
+ name: "label",
4155
+ jsonPath: ["label"],
4156
+ description: "The field's new display name, which cannot be empty",
4157
+ valueName: "value",
4158
+ nullable: true,
4159
+ schema: z.string()
4160
+ }]
2260
4161
  }),
2261
4162
  defineOperation({
2262
4163
  name: "delete",
2263
4164
  summary: "Delete item field",
4165
+ example: "hardfin item field delete item_7Hq2Lm9XcR4tWz8K pfield_2wn8kq4lxp7rtz3m",
2264
4166
  method: "DELETE",
2265
4167
  path: "/item/{itemKey}/field/{fieldKey}",
2266
4168
  pathParameters: [{
@@ -2273,7 +4175,7 @@ const surfaceCommands = [
2273
4175
  required: true
2274
4176
  }],
2275
4177
  queryFlags: [],
2276
- takesBody: false
4178
+ bodyFlags: []
2277
4179
  })
2278
4180
  ]
2279
4181
  }
@@ -2289,6 +4191,7 @@ const surfaceCommands = [
2289
4191
  defineOperation({
2290
4192
  name: "list",
2291
4193
  summary: "Get location listing",
4194
+ example: "hardfin location list --limit 10",
2292
4195
  method: "GET",
2293
4196
  path: "/location",
2294
4197
  pathParameters: [],
@@ -2298,21 +4201,21 @@ const surfaceCommands = [
2298
4201
  queryName: "page",
2299
4202
  description: "The page to return, starting at 1",
2300
4203
  valueName: "number",
2301
- schema: z.coerce.number()
4204
+ schema: z.coerce.number().int()
2302
4205
  },
2303
4206
  {
2304
4207
  name: "limit",
2305
4208
  queryName: "limit",
2306
4209
  description: "The number of records per page, from 1 to 100",
2307
4210
  valueName: "number",
2308
- schema: z.coerce.number()
4211
+ schema: z.coerce.number().int().min(1).max(100)
2309
4212
  },
2310
4213
  {
2311
4214
  name: "sort-by",
2312
4215
  queryName: "sortBy",
2313
4216
  description: "The field to sort by",
2314
4217
  valueName: "value",
2315
- schema: z.enum([
4218
+ schema: toEnum([
2316
4219
  "name",
2317
4220
  "company",
2318
4221
  "assetCount"
@@ -2323,14 +4226,14 @@ const surfaceCommands = [
2323
4226
  queryName: "sortOrder",
2324
4227
  description: "The sort direction",
2325
4228
  valueName: "value",
2326
- schema: z.enum(["ASC", "DESC"])
4229
+ schema: toEnum(["ASC", "DESC"])
2327
4230
  },
2328
4231
  {
2329
4232
  name: "archived",
2330
4233
  queryName: "archived",
2331
4234
  description: "Whether to return unarchived records, archived records, or all of them",
2332
4235
  valueName: "value",
2333
- schema: z.enum([
4236
+ schema: toEnum([
2334
4237
  "all",
2335
4238
  "false",
2336
4239
  "true"
@@ -2341,7 +4244,7 @@ const surfaceCommands = [
2341
4244
  queryName: "isTransient",
2342
4245
  description: "Whether to return permanent locations (false), transient locations (true), or both (all)",
2343
4246
  valueName: "value",
2344
- schema: z.enum([
4247
+ schema: toEnum([
2345
4248
  "all",
2346
4249
  "false",
2347
4250
  "true"
@@ -2377,20 +4280,150 @@ const surfaceCommands = [
2377
4280
  schema: z.array(z.string())
2378
4281
  }
2379
4282
  ],
2380
- takesBody: false
4283
+ bodyFlags: []
2381
4284
  }),
2382
4285
  defineOperation({
2383
4286
  name: "create",
2384
4287
  summary: "Create location",
4288
+ example: "hardfin location create",
2385
4289
  method: "POST",
2386
4290
  path: "/location",
2387
4291
  pathParameters: [],
2388
4292
  queryFlags: [],
2389
- takesBody: true
4293
+ bodyFlags: [
4294
+ {
4295
+ name: "address-line1",
4296
+ jsonPath: ["address", "addressLine1"],
4297
+ description: "The first line of the street address",
4298
+ valueName: "value",
4299
+ nullable: true,
4300
+ schema: z.string()
4301
+ },
4302
+ {
4303
+ name: "address-line2",
4304
+ jsonPath: ["address", "addressLine2"],
4305
+ description: "The second line of the street address, such as a suite",
4306
+ valueName: "value",
4307
+ nullable: true,
4308
+ schema: z.string()
4309
+ },
4310
+ {
4311
+ name: "address-city",
4312
+ jsonPath: ["address", "city"],
4313
+ description: "The city",
4314
+ valueName: "value",
4315
+ nullable: true,
4316
+ schema: z.string()
4317
+ },
4318
+ {
4319
+ name: "address-country",
4320
+ jsonPath: ["address", "country"],
4321
+ description: "The country",
4322
+ valueName: "value",
4323
+ nullable: true,
4324
+ schema: z.string()
4325
+ },
4326
+ {
4327
+ name: "address-formatted-address",
4328
+ jsonPath: ["address", "formattedAddress"],
4329
+ description: "The whole address on one line",
4330
+ valueName: "value",
4331
+ nullable: true,
4332
+ schema: z.string()
4333
+ },
4334
+ {
4335
+ name: "address-postal-code",
4336
+ jsonPath: ["address", "postalCode"],
4337
+ description: "The postal or ZIP code",
4338
+ valueName: "value",
4339
+ nullable: true,
4340
+ schema: z.string()
4341
+ },
4342
+ {
4343
+ name: "address-state",
4344
+ jsonPath: ["address", "state"],
4345
+ description: "The state or region",
4346
+ valueName: "value",
4347
+ nullable: true,
4348
+ schema: z.string()
4349
+ },
4350
+ {
4351
+ name: "consignee",
4352
+ jsonPath: ["consignee"],
4353
+ description: "The ID of the customer a zone is designated for, such as for reservations, provisioning or a 3PL",
4354
+ valueName: "value",
4355
+ nullable: true,
4356
+ schema: z.string()
4357
+ },
4358
+ {
4359
+ name: "customer-id",
4360
+ jsonPath: ["customerId"],
4361
+ description: "The ID of the customer to assign a site to, or null for your organization's own site",
4362
+ valueName: "value",
4363
+ nullable: true,
4364
+ schema: z.string()
4365
+ },
4366
+ {
4367
+ name: "description",
4368
+ jsonPath: ["description"],
4369
+ description: "A free-form description of a zone",
4370
+ valueName: "value",
4371
+ nullable: true,
4372
+ schema: z.string()
4373
+ },
4374
+ {
4375
+ name: "is-inventory",
4376
+ jsonPath: ["isInventory"],
4377
+ description: "Whether assets at the location count as inventory for reporting",
4378
+ negatable: true,
4379
+ schema: z.boolean()
4380
+ },
4381
+ {
4382
+ name: "is-inventory-override",
4383
+ jsonPath: ["isInventoryOverride"],
4384
+ description: "Whether a zone sets its own isInventory rather than inheriting its parent site's, which a site refuses",
4385
+ negatable: true,
4386
+ schema: z.boolean()
4387
+ },
4388
+ {
4389
+ name: "is-transient",
4390
+ jsonPath: ["isTransient"],
4391
+ description: "Whether assets make only occasional or temporary stops at the location, which hides it from location lists by default",
4392
+ negatable: true,
4393
+ schema: z.boolean()
4394
+ },
4395
+ {
4396
+ name: "name",
4397
+ jsonPath: ["name"],
4398
+ description: "The location's display name",
4399
+ valueName: "value",
4400
+ schema: z.string()
4401
+ },
4402
+ {
4403
+ name: "parent-location-id",
4404
+ jsonPath: ["parentLocationId"],
4405
+ description: "The ID of the site a zone belongs to, required for a zone and refused for a site",
4406
+ valueName: "value",
4407
+ nullable: true,
4408
+ schema: z.string()
4409
+ },
4410
+ {
4411
+ name: "type",
4412
+ jsonPath: ["type"],
4413
+ description: "SITE for a site, or ZONE for a zone within a site",
4414
+ valueName: "value",
4415
+ schema: toEnum([
4416
+ "SITE",
4417
+ "UNKNOWN",
4418
+ "ZONE"
4419
+ ])
4420
+ }
4421
+ ]
2390
4422
  }),
2391
4423
  defineOperation({
2392
4424
  name: "get",
2393
4425
  summary: "Get location",
4426
+ example: "hardfin location get loc_4f9Xk2mQ7pLr8sTz",
2394
4427
  method: "GET",
2395
4428
  path: "/location/{locationKey}",
2396
4429
  pathParameters: [{
@@ -2399,11 +4432,12 @@ const surfaceCommands = [
2399
4432
  required: true
2400
4433
  }],
2401
4434
  queryFlags: [],
2402
- takesBody: false
4435
+ bodyFlags: []
2403
4436
  }),
2404
4437
  defineOperation({
2405
4438
  name: "update",
2406
4439
  summary: "Patch location",
4440
+ example: "hardfin location update loc_4f9Xk2mQ7pLr8sTz",
2407
4441
  method: "PATCH",
2408
4442
  path: "/location/{locationKey}",
2409
4443
  pathParameters: [{
@@ -2412,7 +4446,139 @@ const surfaceCommands = [
2412
4446
  required: true
2413
4447
  }],
2414
4448
  queryFlags: [],
2415
- takesBody: true
4449
+ bodyFlags: [
4450
+ {
4451
+ name: "address-line1",
4452
+ jsonPath: ["address", "addressLine1"],
4453
+ description: "The first line of the street address",
4454
+ valueName: "value",
4455
+ nullable: true,
4456
+ schema: z.string()
4457
+ },
4458
+ {
4459
+ name: "address-line2",
4460
+ jsonPath: ["address", "addressLine2"],
4461
+ description: "The second line of the street address, such as a suite",
4462
+ valueName: "value",
4463
+ nullable: true,
4464
+ schema: z.string()
4465
+ },
4466
+ {
4467
+ name: "address-city",
4468
+ jsonPath: ["address", "city"],
4469
+ description: "The city",
4470
+ valueName: "value",
4471
+ nullable: true,
4472
+ schema: z.string()
4473
+ },
4474
+ {
4475
+ name: "address-country",
4476
+ jsonPath: ["address", "country"],
4477
+ description: "The country",
4478
+ valueName: "value",
4479
+ nullable: true,
4480
+ schema: z.string()
4481
+ },
4482
+ {
4483
+ name: "address-formatted-address",
4484
+ jsonPath: ["address", "formattedAddress"],
4485
+ description: "The whole address on one line",
4486
+ valueName: "value",
4487
+ nullable: true,
4488
+ schema: z.string()
4489
+ },
4490
+ {
4491
+ name: "address-postal-code",
4492
+ jsonPath: ["address", "postalCode"],
4493
+ description: "The postal or ZIP code",
4494
+ valueName: "value",
4495
+ nullable: true,
4496
+ schema: z.string()
4497
+ },
4498
+ {
4499
+ name: "address-state",
4500
+ jsonPath: ["address", "state"],
4501
+ description: "The state or region",
4502
+ valueName: "value",
4503
+ nullable: true,
4504
+ schema: z.string()
4505
+ },
4506
+ {
4507
+ name: "consignee",
4508
+ jsonPath: ["consignee"],
4509
+ description: "The ID of the customer a zone is designated for, such as for reservations, provisioning or a 3PL",
4510
+ valueName: "value",
4511
+ nullable: true,
4512
+ schema: z.string()
4513
+ },
4514
+ {
4515
+ name: "customer-id",
4516
+ jsonPath: ["customerId"],
4517
+ description: "The ID of the customer to assign a site to, or null for your organization's own site",
4518
+ valueName: "value",
4519
+ nullable: true,
4520
+ schema: z.string()
4521
+ },
4522
+ {
4523
+ name: "description",
4524
+ jsonPath: ["description"],
4525
+ description: "A free-form description of a zone",
4526
+ valueName: "value",
4527
+ nullable: true,
4528
+ schema: z.string()
4529
+ },
4530
+ {
4531
+ name: "is-archived",
4532
+ jsonPath: ["isArchived"],
4533
+ description: "Whether the location is archived, and archiving a site archives its zones",
4534
+ negatable: true,
4535
+ nullable: true,
4536
+ schema: z.boolean()
4537
+ },
4538
+ {
4539
+ name: "is-inventory",
4540
+ jsonPath: ["isInventory"],
4541
+ description: "Whether assets at the location count as inventory for reporting",
4542
+ negatable: true,
4543
+ nullable: true,
4544
+ schema: z.boolean()
4545
+ },
4546
+ {
4547
+ name: "is-inventory-override",
4548
+ jsonPath: ["isInventoryOverride"],
4549
+ description: "Whether a zone sets its own isInventory rather than inheriting its parent site's, which a site refuses",
4550
+ negatable: true,
4551
+ nullable: true,
4552
+ schema: z.boolean()
4553
+ },
4554
+ {
4555
+ name: "is-transient",
4556
+ jsonPath: ["isTransient"],
4557
+ description: "Whether assets make only occasional or temporary stops at the location, which hides it from location lists by default",
4558
+ negatable: true,
4559
+ nullable: true,
4560
+ schema: z.boolean()
4561
+ },
4562
+ {
4563
+ name: "name",
4564
+ jsonPath: ["name"],
4565
+ description: "The location's display name",
4566
+ valueName: "value",
4567
+ nullable: true,
4568
+ schema: z.string()
4569
+ },
4570
+ {
4571
+ name: "type",
4572
+ jsonPath: ["type"],
4573
+ description: "SITE for a site, or ZONE for a zone within a site",
4574
+ valueName: "value",
4575
+ schema: toEnum([
4576
+ "SITE",
4577
+ "UNKNOWN",
4578
+ "ZONE"
4579
+ ])
4580
+ }
4581
+ ]
2416
4582
  }),
2417
4583
  {
2418
4584
  name: "zones",
@@ -2423,6 +4589,7 @@ const surfaceCommands = [
2423
4589
  subcommands: [defineOperation({
2424
4590
  name: "list",
2425
4591
  summary: "Get zones",
4592
+ example: "hardfin location zones list loc_4f9Xk2mQ7pLr8sTz",
2426
4593
  method: "GET",
2427
4594
  path: "/location/{locationKey}/zones",
2428
4595
  pathParameters: [{
@@ -2435,17 +4602,34 @@ const surfaceCommands = [
2435
4602
  queryName: "archived",
2436
4603
  description: "Whether to return unarchived zones, archived zones, or all of them",
2437
4604
  valueName: "value",
2438
- schema: z.enum([
4605
+ schema: toEnum([
2439
4606
  "all",
2440
4607
  "false",
2441
4608
  "true"
2442
4609
  ])
2443
4610
  }],
2444
- takesBody: false
4611
+ bodyFlags: []
2445
4612
  })]
2446
4613
  }
2447
4614
  ]
2448
4615
  },
4616
+ {
4617
+ name: "token",
4618
+ summary: "Token commands",
4619
+ arguments: [],
4620
+ flags: [],
4621
+ examples: [],
4622
+ subcommands: [defineOperation({
4623
+ name: "get",
4624
+ summary: "Report the credential this CLI is calling with",
4625
+ example: "hardfin token get",
4626
+ method: "GET",
4627
+ path: "/token",
4628
+ pathParameters: [],
4629
+ queryFlags: [],
4630
+ bodyFlags: []
4631
+ })]
4632
+ },
2449
4633
  {
2450
4634
  name: "url-link",
2451
4635
  summary: "URL link commands",
@@ -2456,6 +4640,7 @@ const surfaceCommands = [
2456
4640
  defineOperation({
2457
4641
  name: "get",
2458
4642
  summary: "Get URL link by key",
4643
+ example: "hardfin url-link get link_7hq2mx9pcr4stz8w",
2459
4644
  method: "GET",
2460
4645
  path: "/url-link/{linkKey}",
2461
4646
  pathParameters: [{
@@ -2464,11 +4649,12 @@ const surfaceCommands = [
2464
4649
  required: true
2465
4650
  }],
2466
4651
  queryFlags: [],
2467
- takesBody: false
4652
+ bodyFlags: []
2468
4653
  }),
2469
4654
  defineOperation({
2470
4655
  name: "update",
2471
4656
  summary: "Update URL link",
4657
+ example: "hardfin url-link update link_7hq2mx9pcr4stz8w",
2472
4658
  method: "PATCH",
2473
4659
  path: "/url-link/{linkKey}",
2474
4660
  pathParameters: [{
@@ -2477,11 +4663,26 @@ const surfaceCommands = [
2477
4663
  required: true
2478
4664
  }],
2479
4665
  queryFlags: [],
2480
- takesBody: true
4666
+ bodyFlags: [{
4667
+ name: "name",
4668
+ jsonPath: ["name"],
4669
+ description: "The link's display name, or null to show the address instead",
4670
+ valueName: "value",
4671
+ nullable: true,
4672
+ schema: z.string()
4673
+ }, {
4674
+ name: "url",
4675
+ jsonPath: ["url"],
4676
+ description: "The address the link points to, which cannot be empty or null",
4677
+ valueName: "value",
4678
+ nullable: true,
4679
+ schema: z.string()
4680
+ }]
2481
4681
  }),
2482
4682
  defineOperation({
2483
4683
  name: "delete",
2484
4684
  summary: "Delete URL link",
4685
+ example: "hardfin url-link delete link_7hq2mx9pcr4stz8w",
2485
4686
  method: "DELETE",
2486
4687
  path: "/url-link/{linkKey}",
2487
4688
  pathParameters: [{
@@ -2490,38 +4691,12 @@ const surfaceCommands = [
2490
4691
  required: true
2491
4692
  }],
2492
4693
  queryFlags: [],
2493
- takesBody: false
4694
+ bodyFlags: []
2494
4695
  })
2495
4696
  ]
2496
4697
  }
2497
4698
  ];
2498
4699
  //#endregion
2499
- //#region src/auth/jwt.ts
2500
- /**
2501
- * toClaims reads an access token's payload for display. Nothing here verifies the
2502
- * signature, because the API is what decides whether a token is good.
2503
- */
2504
- function toClaims(token) {
2505
- const payload = token.split(".")[1];
2506
- if (!payload) return {};
2507
- try {
2508
- const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
2509
- return {
2510
- expiresAt: typeof decoded["exp"] === "number" ? decoded["exp"] * 1e3 : void 0,
2511
- issuedAt: typeof decoded["iat"] === "number" ? decoded["iat"] * 1e3 : void 0,
2512
- scopes: toScopes(decoded),
2513
- subject: typeof decoded["sub"] === "string" ? decoded["sub"] : void 0
2514
- };
2515
- } catch {
2516
- return {};
2517
- }
2518
- }
2519
- /** toScopes reads scp, which is where Hardfin puts an access token's scopes. */
2520
- function toScopes(decoded) {
2521
- if (Array.isArray(decoded["scp"])) return decoded["scp"].map(String);
2522
- return typeof decoded["scope"] === "string" ? decoded["scope"].split(" ") : void 0;
2523
- }
2524
- //#endregion
2525
4700
  //#region src/system/host.ts
2526
4701
  const BYTES_PER_GB = 1024 ** 3;
2527
4702
  /** toDistribution reads the name a Linux distribution gives itself. */
@@ -2565,8 +4740,8 @@ function toFile(path) {
2565
4740
  //#endregion
2566
4741
  //#region src/command/status.ts
2567
4742
  /**
2568
- * Hardfin expires an unused refresh token after this long. It is the server's rule, not the
2569
- * CLI's, and the token endpoint reports no expiry, so what this produces is an estimate.
4743
+ * What the CLI falls back to for an older sign in, stored before the server began reporting
4744
+ * when a refresh token expires. Hardfin's own rule, and an estimate rather than a fact.
2570
4745
  */
2571
4746
  const REFRESH_SLIDING_DAYS = 90;
2572
4747
  const statusCommand = defineCommand({
@@ -2629,27 +4804,24 @@ function toEnvironmentReport() {
2629
4804
  apiVersion: API_VERSION,
2630
4805
  node: process.version,
2631
4806
  platform: process.platform,
2632
- interactive: Boolean(process.stdin.isTTY)
4807
+ interactive: process.stdin.isTTY
2633
4808
  };
2634
4809
  }
2635
4810
  /** toConfigurationReport names every setting, its source, and never a secret's value. */
2636
4811
  function toConfigurationReport(resolved) {
2637
- const entries = Object.entries(resolved.settings).map(([key, value]) => {
4812
+ const report = { configFile: CONFIG_FILE };
4813
+ for (const [key, value] of Object.entries(resolved.settings)) {
2638
4814
  const from = resolved.sources[key];
2639
- if (key === "apiKey") return [key, {
4815
+ report[key] = key === "apiKey" ? {
2640
4816
  set: value !== void 0,
2641
4817
  fingerprint: toFingerprint(value),
2642
4818
  from
2643
- }];
2644
- return [key, {
4819
+ } : {
2645
4820
  value: value ?? null,
2646
4821
  from
2647
- }];
2648
- });
2649
- return {
2650
- ...Object.fromEntries(entries),
2651
- configFile: CONFIG_FILE
2652
- };
4822
+ };
4823
+ }
4824
+ return report;
2653
4825
  }
2654
4826
  function toCredentialReport(apiKey, stored) {
2655
4827
  if (apiKey) return {
@@ -2670,8 +4842,8 @@ function toCredentialReport(apiKey, stored) {
2670
4842
  fingerprint: toFingerprint(stored.refreshToken),
2671
4843
  signedInAt: stored.signedInAt ?? null,
2672
4844
  renewedAt: stored.renewedAt ?? null,
2673
- refreshExpiresAt: toRefreshExpiry(stored.renewedAt),
2674
- refreshExpiryIsEstimated: true
4845
+ refreshExpiresAt: stored.expiresAt ?? toRefreshExpiry(stored.renewedAt),
4846
+ refreshExpiryIsEstimated: stored.expiresAt === void 0
2675
4847
  };
2676
4848
  }
2677
4849
  function toServerReport(metadata) {
@@ -2745,8 +4917,7 @@ function toFlattened(value) {
2745
4917
  if (value === null || typeof value !== "object" || Array.isArray(value)) return;
2746
4918
  const holder = value;
2747
4919
  if (holder.from === void 0) return;
2748
- const shown = holder.value ?? (holder.set ? holder.fingerprint ?? "set" : "not set");
2749
- return `${String(shown)} (${holder.from})`;
4920
+ return `${toText(holder.value ?? (holder.set ? holder.fingerprint ?? "set" : "not set"))} (${holder.from})`;
2750
4921
  }
2751
4922
  /** toLines lays the report out for a person, one indented line per value. */
2752
4923
  function toLines(report, depth = 0) {
@@ -2776,7 +4947,10 @@ const commands = [
2776
4947
  ...surfaceCommands,
2777
4948
  apiCommand,
2778
4949
  configCommand,
2779
- agentGuideCommand
4950
+ agentGuideCommand,
4951
+ completionCommand,
4952
+ mcpCommand,
4953
+ completeCommand
2780
4954
  ];
2781
4955
  //#endregion
2782
4956
  //#region src/command/validate.ts
@@ -2789,13 +4963,16 @@ function toRejectedFlag(command, flags) {
2789
4963
  }
2790
4964
  }
2791
4965
  //#endregion
2792
- //#region src/cli.ts
2793
- const program = new Command();
2794
- program.name("hardfin").description("Call the Hardfin API from a terminal or an agent").version(version$1, "-v, --version").option("--api-url <url>", "The API to call, whose host also holds the authorization server").option("--issuer-url <url>", "The authorization server, when it does not sit at the API's host").showHelpAfterError().enablePositionalOptions();
2795
- for (const command of commands) program.addCommand(toProgram(command));
2796
- await program.parseAsync(process.argv);
4966
+ //#region src/cli/program.ts
4967
+ /** toCli builds the parser from the registry, which is what every surface reads. */
4968
+ function toCli(entries = commands) {
4969
+ const program = new Command();
4970
+ program.name("hardfin").description("Call the Hardfin API from a terminal or an agent").version(version$1, "-v, --version").option("--api-url <url>", "The API to call, whose host also holds the authorization server").option("--issuer-url <url>", "The authorization server, when it does not sit at the API's host").showHelpAfterError().enablePositionalOptions();
4971
+ for (const command of entries) program.addCommand(toProgram(program, command), { hidden: command.hidden });
4972
+ return program;
4973
+ }
2797
4974
  /** toProgram wires one registry command into the parser. */
2798
- function toProgram(command) {
4975
+ function toProgram(root, command) {
2799
4976
  const program = new Command(command.name).summary(command.summary).description(command.description ?? command.summary);
2800
4977
  for (const argument of command.arguments) {
2801
4978
  const name = argument.variadic ? `${argument.name}...` : argument.name;
@@ -2805,21 +4982,22 @@ function toProgram(command) {
2805
4982
  const short = flag.short ? `-${flag.short}, ` : "";
2806
4983
  const value = flag.valueName ? ` <${flag.valueName}>` : "";
2807
4984
  const option = new Option(`${short}--${flag.name}${value}`, flag.description);
4985
+ if (flag.negatable) program.addOption(new Option(`--no-${flag.name}`, `${flag.description}, turned off`));
2808
4986
  if (flag.repeatable) option.argParser(collect);
2809
4987
  if (flag.defaultValue !== void 0) option.default(flag.defaultValue);
2810
4988
  program.addOption(option);
2811
4989
  }
2812
4990
  for (const example of command.examples) program.addHelpText("after", `\n${example.description}:\n $ ${example.command}`);
2813
- for (const subcommand of command.subcommands ?? []) program.addCommand(toProgram(subcommand));
4991
+ for (const subcommand of command.subcommands ?? []) program.addCommand(toProgram(root, subcommand), { hidden: subcommand.hidden });
2814
4992
  if (!command.run) return program;
2815
4993
  program.action(async (...parsed) => {
2816
4994
  const flags = parsed[parsed.length - 2] ?? {};
2817
4995
  const args = parsed.slice(0, parsed.length - 2).flatMap(toArgumentList);
2818
- process.exitCode = await toExitCode(command, args, flags);
4996
+ process.exitCode = await toExitCode(root, command, args, flags);
2819
4997
  });
2820
4998
  return program;
2821
4999
  }
2822
- async function toExitCode(command, args, flags) {
5000
+ async function toExitCode(root, command, args, flags) {
2823
5001
  const isJSON = isJSONOutput(flags);
2824
5002
  const rejected = toRejectedFlag(command, flags);
2825
5003
  if (rejected) {
@@ -2828,8 +5006,8 @@ async function toExitCode(command, args, flags) {
2828
5006
  }
2829
5007
  try {
2830
5008
  const resolved = toSettings({
2831
- apiUrl: program.opts()["apiUrl"],
2832
- issuerUrl: program.opts()["issuerUrl"]
5009
+ apiUrl: root.opts()["apiUrl"],
5010
+ issuerUrl: root.opts()["issuerUrl"]
2833
5011
  });
2834
5012
  return await command.run?.({
2835
5013
  args,
@@ -2845,10 +5023,13 @@ async function toExitCode(command, args, flags) {
2845
5023
  }
2846
5024
  function toArgumentList(value) {
2847
5025
  if (Array.isArray(value)) return value.map(String);
2848
- return value === void 0 ? [] : [String(value)];
5026
+ return value === void 0 ? [] : [toText(value)];
2849
5027
  }
2850
5028
  function collect(value, previous) {
2851
5029
  return [...previous ?? [], value];
2852
5030
  }
2853
5031
  //#endregion
5032
+ //#region src/cli.ts
5033
+ await toCli().parseAsync(process.argv);
5034
+ //#endregion
2854
5035
  export {};