@hardfin/cli 0.1.0-dev.19 → 0.1.0-dev.21

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 +6 -2
  2. package/dist/cli.js +445 -207
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -63,8 +63,7 @@ hardfin asset ownership create ast_4f9xk2mq7plr8stz \
63
63
  --customer-id cst_a1 --date 2026-09-23 --sale-price 1500.00
64
64
 
65
65
  hardfin asset move execute create \
66
- --move assetId=ast_a1,locationId=loc_x \
67
- --move assetId=ast_b2,locationId=loc_y
66
+ --move assetId=0192f7d4-3b6a-7c1e-9f2d-5a8b4c6e7d70,originId=0192f7d4-3b6a-7c1e-9f2d-5a8b4c6e7d71,destinationId=0192f7d4-3b6a-7c1e-9f2d-5a8b4c6e7d72,shipAt=2026-09-24T10:00:00Z
68
67
  ```
69
68
 
70
69
  A value is sent as the type the document names. `--useful-life 36` sends the number 36. A
@@ -343,6 +342,11 @@ The CLI reads whatever `config.local.json` and `.env` sit in the directory you r
343
342
  A directory you do not control can therefore point the CLI at a server you do not expect, so
344
343
  run `hardfin config` when a command reaches somewhere surprising.
345
344
 
345
+ A sign in is not carried there. An access token is sent only to the host that issued it, and
346
+ only over https unless that host is this machine. Pointing `apiUrl` elsewhere gets a refusal
347
+ naming both hosts, rather than a token sent to a stranger. An API key is sent wherever you
348
+ point it, because setting one is a deliberate act.
349
+
346
350
  ## Working on the CLI
347
351
 
348
352
  ```sh
package/dist/cli.js CHANGED
@@ -139,9 +139,9 @@ function writeData(value) {
139
139
  process.stdout.write(`${JSON.stringify(value ?? null, null, 2)}\n`);
140
140
  }
141
141
  /** writeFailure prints why a command failed on stderr, as text or as JSON. */
142
- function writeFailure(message, isJSON, errors, requestId) {
142
+ function writeFailure(message, isJSON, errors, requestId, status) {
143
143
  if (!isJSON) {
144
- process.stderr.write(`error: ${message}\n`);
144
+ process.stderr.write(`error: ${message}${status === void 0 ? "" : ` (HTTP ${status})`}\n`);
145
145
  for (const entry of errors?.slice(1) ?? []) process.stderr.write(` ${entry.error} (${entry.statusCode})\n`);
146
146
  if (requestId) process.stderr.write(` request ${requestId}\n`);
147
147
  return;
@@ -151,7 +151,8 @@ function writeFailure(message, isJSON, errors, requestId) {
151
151
  error: message,
152
152
  statusCode: 0
153
153
  }],
154
- requestId: requestId ?? null
154
+ requestId: requestId ?? null,
155
+ status: status ?? null
155
156
  };
156
157
  process.stderr.write(`${JSON.stringify(payload, null, 2)}\n`);
157
158
  }
@@ -289,112 +290,6 @@ function toScopes(decoded) {
289
290
  return typeof decoded["scope"] === "string" ? decoded["scope"].split(" ") : void 0;
290
291
  }
291
292
  //#endregion
292
- //#region src/auth/metadata.ts
293
- const METADATA_PATH = "/.well-known/oauth-authorization-server";
294
- const AuthorizationServerMetadata = z.looseObject({
295
- issuer: z.string(),
296
- authorization_endpoint: z.string(),
297
- token_endpoint: z.string(),
298
- revocation_endpoint: z.string().optional(),
299
- device_authorization_endpoint: z.string().optional(),
300
- scopes_supported: z.array(z.string()).optional(),
301
- grant_types_supported: z.array(z.string()).optional(),
302
- code_challenge_methods_supported: z.array(z.string()).optional()
303
- });
304
- /** DiscoveryFailure is an authorization server that cannot be read or does not describe itself. */
305
- var DiscoveryFailure = class extends Error {
306
- constructor(message) {
307
- super(message);
308
- this.name = "DiscoveryFailure";
309
- }
310
- };
311
- /** toMetadata reads what an authorization server says about itself. */
312
- async function toMetadata(issuer) {
313
- const url = `${issuer.replace(/\/+$/, "")}${METADATA_PATH}`;
314
- let response;
315
- try {
316
- response = await fetch(url, { headers: { Accept: "application/json" } });
317
- } catch (error) {
318
- throw new DiscoveryFailure(`${url} could not be reached: ${error instanceof Error ? error.message : String(error)}`);
319
- }
320
- if (!response.ok) throw new DiscoveryFailure(`${url} answered ${response.status}, so this host publishes no authorization server`);
321
- const parsed = AuthorizationServerMetadata.safeParse(await response.json().catch(() => void 0));
322
- if (!parsed.success) throw new DiscoveryFailure(`${url} does not describe an authorization server`);
323
- if (parsed.data.issuer.replace(/\/+$/, "") !== issuer.replace(/\/+$/, "")) throw new DiscoveryFailure(`${url} names issuer ${parsed.data.issuer}, which is not the host it was read from`);
324
- return parsed.data;
325
- }
326
- //#endregion
327
- //#region src/auth/grant.ts
328
- const TokenResponse = z.looseObject({
329
- access_token: z.string(),
330
- token_type: z.string(),
331
- expires_in: z.number().optional(),
332
- refresh_token: z.string().optional(),
333
- refresh_token_expires_in: z.number().optional(),
334
- scope: z.string().optional()
335
- });
336
- /** GrantFailure is a token request the authorization server refused. */
337
- var GrantFailure = class extends Error {
338
- code;
339
- constructor(code, description) {
340
- super(description ? `${code}: ${description}` : code);
341
- this.name = "GrantFailure";
342
- this.code = code;
343
- }
344
- };
345
- /** toTokensFromCode trades an authorization code for tokens. */
346
- async function toTokensFromCode(tokenUrl, clientId, code, redirectUri, pkce) {
347
- return await toTokens(tokenUrl, {
348
- grant_type: "authorization_code",
349
- client_id: clientId,
350
- code,
351
- redirect_uri: redirectUri,
352
- code_verifier: pkce.verifier
353
- });
354
- }
355
- /** toTokensFromRefresh trades a refresh token for a fresh pair. */
356
- async function toTokensFromRefresh(tokenUrl, clientId, refreshToken) {
357
- return await toTokens(tokenUrl, {
358
- grant_type: "refresh_token",
359
- client_id: clientId,
360
- refresh_token: refreshToken
361
- });
362
- }
363
- /** revoke tells the server to forget a token, so signing out reaches every machine. */
364
- async function revoke(revocationUrl, clientId, token) {
365
- await fetch(revocationUrl, {
366
- method: "POST",
367
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
368
- body: new URLSearchParams({
369
- client_id: clientId,
370
- token,
371
- token_type_hint: "refresh_token"
372
- })
373
- });
374
- }
375
- /** toTokens asks the token endpoint, which answers a flat RFC 6749 body, not the envelope. */
376
- async function toTokens(url, form) {
377
- const response = await fetch(url, {
378
- method: "POST",
379
- headers: {
380
- "Content-Type": "application/x-www-form-urlencoded",
381
- Accept: "application/json"
382
- },
383
- body: new URLSearchParams(form)
384
- });
385
- const body = await response.json().catch(() => void 0);
386
- if (!response.ok) throw new GrantFailure(toText(body?.["error"] ?? `http_${response.status}`), body?.["error_description"] === void 0 ? void 0 : toText(body["error_description"]));
387
- const parsed = TokenResponse.safeParse(body);
388
- if (!parsed.success) throw new GrantFailure("invalid_response", "the token endpoint did not answer with a token");
389
- return {
390
- accessToken: parsed.data.access_token,
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,
393
- scope: parsed.data.scope,
394
- expiresAt: parsed.data.expires_in === void 0 ? void 0 : Date.now() + parsed.data.expires_in * 1e3
395
- };
396
- }
397
- //#endregion
398
293
  //#region src/credential/store.ts
399
294
  const load = createRequire(import.meta.url);
400
295
  /** The service a keyring entry is filed under, alongside the issuer it belongs to. */
@@ -414,7 +309,7 @@ function toCredentialPath() {
414
309
  * family lifetime runs from. Callers hold the credential lock, which is what makes a write
415
310
  * from another process merge rather than disappear.
416
311
  */
417
- function keep(issuer, refreshToken, expiresAt) {
312
+ function keep$1(issuer, refreshToken, expiresAt) {
418
313
  const now = (/* @__PURE__ */ new Date()).toISOString();
419
314
  const record = {
420
315
  refreshToken,
@@ -526,6 +421,147 @@ function writeFile(held) {
526
421
  renameSync(pending, path);
527
422
  }
528
423
  //#endregion
424
+ //#region src/auth/metadata.ts
425
+ const METADATA_PATH = "/.well-known/oauth-authorization-server";
426
+ const AuthorizationServerMetadata = z.looseObject({
427
+ issuer: z.string(),
428
+ authorization_endpoint: z.string(),
429
+ token_endpoint: z.string(),
430
+ revocation_endpoint: z.string().optional(),
431
+ device_authorization_endpoint: z.string().optional(),
432
+ scopes_supported: z.array(z.string()).optional(),
433
+ grant_types_supported: z.array(z.string()).optional(),
434
+ code_challenge_methods_supported: z.array(z.string()).optional()
435
+ });
436
+ /** DiscoveryFailure is an authorization server that cannot be read or does not describe itself. */
437
+ var DiscoveryFailure = class extends Error {
438
+ constructor(message) {
439
+ super(message);
440
+ this.name = "DiscoveryFailure";
441
+ }
442
+ };
443
+ /** How long a discovery document is reused, which is the hour the server asks for. */
444
+ const CACHE_MS = 36e5;
445
+ /** toMetadata reads what an authorization server says about itself. */
446
+ async function toMetadata(issuer) {
447
+ const cached = toCached(issuer);
448
+ if (cached) return cached;
449
+ const url = `${issuer.replace(/\/+$/, "")}${METADATA_PATH}`;
450
+ let response;
451
+ try {
452
+ response = await fetch(url, { headers: { Accept: "application/json" } });
453
+ } catch (error) {
454
+ throw new DiscoveryFailure(`${url} could not be reached: ${error instanceof Error ? error.message : String(error)}`);
455
+ }
456
+ if (!response.ok) throw new DiscoveryFailure(`${url} answered ${response.status}, so this host publishes no authorization server`);
457
+ const parsed = AuthorizationServerMetadata.safeParse(await response.json().catch(() => void 0));
458
+ if (!parsed.success) throw new DiscoveryFailure(`${url} does not describe an authorization server`);
459
+ if (parsed.data.issuer.replace(/\/+$/, "") !== issuer.replace(/\/+$/, "")) throw new DiscoveryFailure(`${url} names issuer ${parsed.data.issuer}, which is not the host it was read from`);
460
+ keep(issuer, parsed.data);
461
+ return parsed.data;
462
+ }
463
+ /** toCachePath names where one server's document is kept, beside the credentials. */
464
+ function toCachePath(issuer) {
465
+ const name = createHash("sha256").update(issuer).digest("hex").slice(0, 16);
466
+ return join(dirname(toCredentialPath()), `metadata-${name}.json`);
467
+ }
468
+ function toCached(issuer) {
469
+ const path = toCachePath(issuer);
470
+ if (!existsSync(path)) return;
471
+ try {
472
+ const held = JSON.parse(readFileSync(path, "utf8"));
473
+ if ((held.readAt ?? 0) + CACHE_MS < Date.now()) return;
474
+ const parsed = AuthorizationServerMetadata.safeParse(held.document);
475
+ return parsed.success ? parsed.data : void 0;
476
+ } catch {
477
+ return;
478
+ }
479
+ }
480
+ function keep(issuer, document) {
481
+ const path = toCachePath(issuer);
482
+ try {
483
+ mkdirSync(dirname(path), {
484
+ recursive: true,
485
+ mode: 448
486
+ });
487
+ writeFileSync(path, JSON.stringify({
488
+ readAt: Date.now(),
489
+ document
490
+ }), { mode: 384 });
491
+ } catch {}
492
+ }
493
+ //#endregion
494
+ //#region src/auth/grant.ts
495
+ const TokenResponse = z.looseObject({
496
+ access_token: z.string(),
497
+ token_type: z.string(),
498
+ expires_in: z.number().optional(),
499
+ refresh_token: z.string().optional(),
500
+ refresh_token_expires_in: z.number().optional(),
501
+ scope: z.string().optional()
502
+ });
503
+ /** GrantFailure is a token request the authorization server refused. */
504
+ var GrantFailure = class extends Error {
505
+ code;
506
+ constructor(code, description) {
507
+ super(description ? `${code}: ${description}` : code);
508
+ this.name = "GrantFailure";
509
+ this.code = code;
510
+ }
511
+ };
512
+ /** toTokensFromCode trades an authorization code for tokens. */
513
+ async function toTokensFromCode(tokenUrl, clientId, code, redirectUri, pkce) {
514
+ return await toTokens(tokenUrl, {
515
+ grant_type: "authorization_code",
516
+ client_id: clientId,
517
+ code,
518
+ redirect_uri: redirectUri,
519
+ code_verifier: pkce.verifier
520
+ });
521
+ }
522
+ /** toTokensFromRefresh trades a refresh token for a fresh pair. */
523
+ async function toTokensFromRefresh(tokenUrl, clientId, refreshToken) {
524
+ return await toTokens(tokenUrl, {
525
+ grant_type: "refresh_token",
526
+ client_id: clientId,
527
+ refresh_token: refreshToken
528
+ });
529
+ }
530
+ /** revoke tells the server to forget a token, so signing out reaches every machine. */
531
+ async function revoke(revocationUrl, clientId, token) {
532
+ await fetch(revocationUrl, {
533
+ method: "POST",
534
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
535
+ body: new URLSearchParams({
536
+ client_id: clientId,
537
+ token,
538
+ token_type_hint: "refresh_token"
539
+ })
540
+ });
541
+ }
542
+ /** toTokens asks the token endpoint, which answers a flat RFC 6749 body, not the envelope. */
543
+ async function toTokens(url, form) {
544
+ const response = await fetch(url, {
545
+ method: "POST",
546
+ headers: {
547
+ "Content-Type": "application/x-www-form-urlencoded",
548
+ Accept: "application/json"
549
+ },
550
+ body: new URLSearchParams(form)
551
+ });
552
+ const body = await response.json().catch(() => void 0);
553
+ if (!response.ok) throw new GrantFailure(toText(body?.["error"] ?? `http_${response.status}`), body?.["error_description"] === void 0 ? void 0 : toText(body["error_description"]));
554
+ const parsed = TokenResponse.safeParse(body);
555
+ if (!parsed.success) throw new GrantFailure("invalid_response", "the token endpoint did not answer with a token");
556
+ return {
557
+ accessToken: parsed.data.access_token,
558
+ refreshToken: parsed.data.refresh_token,
559
+ refreshExpiresAt: parsed.data.refresh_token_expires_in === void 0 ? void 0 : Date.now() + parsed.data.refresh_token_expires_in * 1e3,
560
+ scope: parsed.data.scope,
561
+ expiresAt: parsed.data.expires_in === void 0 ? void 0 : Date.now() + parsed.data.expires_in * 1e3
562
+ };
563
+ }
564
+ //#endregion
529
565
  //#region src/credential/lock.ts
530
566
  /** A lock held longer than this belongs to a process that died, so it is taken. */
531
567
  const STALE_MS = 3e4;
@@ -636,6 +672,7 @@ async function toRequestCredential(settings) {
636
672
  value: settings.apiKey,
637
673
  kind: "api key"
638
674
  };
675
+ refuseForeignHost(settings);
639
676
  const stored = toCredential(settings.issuerUrl);
640
677
  if (!stored) throw new NoCredential("not authenticated. Run hardfin login, or set HARDFIN_API_KEY");
641
678
  return {
@@ -693,11 +730,33 @@ function toDated(tokens) {
693
730
  function store(issuer, tokens, previous) {
694
731
  if (!tokens.refreshToken || tokens.refreshToken === previous) return;
695
732
  try {
696
- keep(issuer, tokens.refreshToken, tokens.refreshExpiresAt);
733
+ keep$1(issuer, tokens.refreshToken, tokens.refreshExpiresAt);
697
734
  } catch (error) {
698
735
  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
736
  }
700
737
  }
738
+ /**
739
+ * refuseForeignHost keeps an access token on the host that issued it. The API URL and the
740
+ * authorization server are configured separately, and a directory's config file can set
741
+ * either, so nothing else stops a token minted for Hardfin being sent somewhere else.
742
+ */
743
+ function refuseForeignHost(settings) {
744
+ const api = toHost(settings.apiUrl);
745
+ const issuer = toHost(settings.issuerUrl);
746
+ if (api === void 0 || issuer === void 0) throw new NoCredential(`${settings.apiUrl} is not a URL this CLI can call`);
747
+ if (api.host !== issuer.host) throw new NoCredential(`this sign in belongs to ${issuer.host}, so it is not sent to ${api.host}. Set HARDFIN_API_KEY to call another host, or point --issuer-url at the server that signs you in`);
748
+ if (api.protocol !== "https:" && !isLoopback(api.hostname)) throw new NoCredential(`${settings.apiUrl} is not https, so a sign in is not sent to it`);
749
+ }
750
+ function toHost(url) {
751
+ try {
752
+ return new URL(url);
753
+ } catch {
754
+ return;
755
+ }
756
+ }
757
+ function isLoopback(hostname) {
758
+ return hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]" || hostname === "localhost";
759
+ }
701
760
  /** forgetHeldTokens drops the access tokens this process is holding. */
702
761
  function forgetHeldTokens() {
703
762
  held.clear();
@@ -838,6 +897,10 @@ async function runApi(input) {
838
897
  writeFailure("a path is required, such as /customer", input.isJSON);
839
898
  return ExitCode.USAGE;
840
899
  }
900
+ if (path.split("/").includes("..")) {
901
+ writeFailure("a path stays inside the API, so it cannot contain ..", input.isJSON);
902
+ return ExitCode.USAGE;
903
+ }
841
904
  const query = toQuery$1(input.flags["field"]);
842
905
  if (query === void 0) {
843
906
  writeFailure("each --field is key=value, such as -f limit=50", input.isJSON);
@@ -867,7 +930,7 @@ async function runApi(input) {
867
930
  return ExitCode.NOT_AUTHENTICATED;
868
931
  }
869
932
  if (error instanceof RequestFailure) {
870
- writeFailure(error.message, input.isJSON, error.errors, error.requestId);
933
+ writeFailure(error.message, input.isJSON, error.errors, error.requestId, error.status);
871
934
  return error.status === 401 ? ExitCode.NOT_AUTHENTICATED : ExitCode.ERROR;
872
935
  }
873
936
  writeFailure(error instanceof Error ? error.message : String(error), input.isJSON);
@@ -993,31 +1056,100 @@ async function runCompletion(input) {
993
1056
  }
994
1057
  /** toScript writes a shell's completion, each one asking __complete for the candidates. */
995
1058
  function toScript(shell) {
996
- if (shell === "bash") return `# hardfin completion for bash
1059
+ return SCRIPTS[shell];
1060
+ }
1061
+ const SCRIPTS = {
1062
+ bash: `# hardfin completion for bash
1063
+ _hardfin_words() {
1064
+ # Bash splits a word at = and :, so the pieces are joined again wherever no space
1065
+ # separates them in the line, and the word under the cursor ends at the cursor.
1066
+ local line=\${COMP_LINE:0:COMP_POINT} offset=0 i word rest gap
1067
+ _hardfin_line_words=()
1068
+
1069
+ for ((i = 0; i <= COMP_CWORD; i++)); do
1070
+ rest=\${line:offset}
1071
+
1072
+ if ((i < COMP_CWORD)); then
1073
+ word=\${COMP_WORDS[i]}
1074
+ gap=\${rest%%"$word"*}
1075
+ offset=$((offset + \${#gap} + \${#word}))
1076
+ else
1077
+ word=\${rest#"\${rest%%[![:space:]]*}"}
1078
+ gap=\${rest%"$word"}
1079
+ fi
1080
+
1081
+ if ((i > 0)) && [[ -z $gap ]]; then
1082
+ _hardfin_line_words[\${#_hardfin_line_words[@]} - 1]+=$word
1083
+ else
1084
+ _hardfin_line_words+=("$word")
1085
+ fi
1086
+ done
1087
+ }
1088
+
997
1089
  _hardfin_complete() {
998
- local words
999
- words=("\${COMP_WORDS[@]:1}")
1000
- COMPREPLY=($(hardfin __complete -- "\${words[@]}" 2>/dev/null))
1090
+ local _hardfin_line_words candidate current prefix breaks
1091
+ _hardfin_words
1092
+ COMPREPLY=()
1093
+
1094
+ while IFS= read -r candidate; do
1095
+ COMPREPLY+=("$candidate")
1096
+ done < <(hardfin __complete -- "\${_hardfin_line_words[@]:1}" 2>/dev/null)
1097
+
1098
+ # Readline replaces only the text after the last = or : in the word, so a candidate
1099
+ # loses what comes before it or the line would repeat that part.
1100
+ current=\${_hardfin_line_words[\${#_hardfin_line_words[@]} - 1]}
1101
+ breaks=\${COMP_WORDBREAKS//[^=:]/}
1102
+
1103
+ if [[ -n $breaks && $current == *["$breaks"]* ]]; then
1104
+ prefix=\${current%"\${current##*["$breaks"]}"}
1105
+ COMPREPLY=("\${COMPREPLY[@]#"$prefix"}")
1106
+ fi
1001
1107
  }
1002
- complete -F _hardfin_complete hardfin`;
1003
- if (shell === "zsh") return `# hardfin completion for zsh
1108
+
1109
+ complete -F _hardfin_complete hardfin`,
1110
+ zsh: `# hardfin completion for zsh
1004
1111
  _hardfin_complete() {
1005
1112
  local -a candidates
1006
- candidates=(\${(f)"$(hardfin __complete -- \${words[2,-1]} 2>/dev/null)"})
1113
+ candidates=(\${(f)"$(hardfin __complete -- "\${(@)words[2,CURRENT]}" 2>/dev/null)"})
1007
1114
  compadd -a candidates
1008
1115
  }
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
1116
+
1117
+ compdef _hardfin_complete hardfin`,
1118
+ fish: `# hardfin completion for fish
1119
+ function __hardfin_complete
1120
+ # Older fish lacks -x, which replaced -o, so it falls back to the older option.
1121
+ set -l words (commandline -xpc 2>/dev/null; or commandline -opc)
1122
+ set -l current (commandline -ct)
1123
+ hardfin __complete -- $words[2..-1] "$current" 2>/dev/null
1124
+ end
1125
+
1126
+ complete -c hardfin -f -a "(__hardfin_complete)"`,
1127
+ powershell: `# hardfin completion for PowerShell
1013
1128
  Register-ArgumentCompleter -Native -CommandName hardfin -ScriptBlock {
1014
1129
  param($wordToComplete, $commandAst, $cursorPosition)
1015
- $words = $commandAst.CommandElements | Select-Object -Skip 1 | ForEach-Object { $_.ToString() }
1130
+
1131
+ # Only the words before the cursor decide what is offered, and PowerShell leaves out the
1132
+ # empty word after a trailing space, so it is added back.
1133
+ $words = @($commandAst.CommandElements | Select-Object -Skip 1 | Where-Object { $_.Extent.StartOffset -lt $cursorPosition } | ForEach-Object { $_.Extent.Text })
1134
+
1135
+ if ($wordToComplete -eq '') {
1136
+ # Before 7.3, and in Legacy mode, an empty argument never reaches the program unless
1137
+ # it is written as a pair of quotes.
1138
+ $isLegacy = $PSVersionTable.PSVersion -lt [version]'7.3' -or $PSNativeCommandArgumentPassing -eq 'Legacy'
1139
+ $words += if ($isLegacy) { '""' } else { '' }
1140
+ }
1141
+
1016
1142
  hardfin __complete -- @words 2>$null | ForEach-Object {
1017
- [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
1143
+ $text = $_
1144
+
1145
+ if ($text -match '[\\s#@$;,''{}()"\`|<>&]') {
1146
+ $text = "'" + ($text -replace "'", "''") + "'"
1147
+ }
1148
+
1149
+ [System.Management.Automation.CompletionResult]::new($text, $_, 'ParameterValue', $_)
1018
1150
  }
1019
- }`;
1020
- }
1151
+ }`
1152
+ };
1021
1153
  //#endregion
1022
1154
  //#region src/command/config.ts
1023
1155
  const configCommand = defineCommand({
@@ -1505,7 +1637,7 @@ async function toSignedIn(input, metadata, tokens) {
1505
1637
  writeFailure("the authorization server issued no refresh token, so this sign in cannot be kept", input.isJSON);
1506
1638
  return ExitCode.ERROR;
1507
1639
  }
1508
- const backend = await withLock(() => keep(input.resolved.settings.issuerUrl, refreshToken, tokens.refreshExpiresAt));
1640
+ const backend = await withLock(() => keep$1(input.resolved.settings.issuerUrl, refreshToken, tokens.refreshExpiresAt));
1509
1641
  if (input.isJSON) {
1510
1642
  writeData({
1511
1643
  signedIn: true,
@@ -1609,6 +1741,17 @@ const TOOL = {
1609
1741
  required: ["args"]
1610
1742
  }
1611
1743
  };
1744
+ /** ProtocolFailure is a line this server answers with a JSON-RPC error rather than a result. */
1745
+ var ProtocolFailure = class extends Error {
1746
+ code;
1747
+ id;
1748
+ constructor(code, message, id) {
1749
+ super(message);
1750
+ this.name = "ProtocolFailure";
1751
+ this.code = code;
1752
+ this.id = id;
1753
+ }
1754
+ };
1612
1755
  /** toResponse answers one request, and answers nothing to a notification. */
1613
1756
  async function toResponse(request, commands, run) {
1614
1757
  const answer = (result) => ({
@@ -1640,7 +1783,19 @@ async function toResponse(request, commands, run) {
1640
1783
  description: "The command tree as JSON",
1641
1784
  mimeType: "application/json"
1642
1785
  }] });
1643
- case "resources/read": return answer(toResource(toText(request.params?.["uri"] ?? ""), commands));
1786
+ case "resources/read": {
1787
+ const uri = toText(request.params?.["uri"] ?? "");
1788
+ const resource = toResource(uri, commands);
1789
+ if (!resource) return {
1790
+ jsonrpc: "2.0",
1791
+ id: request.id ?? null,
1792
+ error: {
1793
+ code: -32002,
1794
+ message: `${uri} is not a resource this server offers`
1795
+ }
1796
+ };
1797
+ return answer(resource);
1798
+ }
1644
1799
  case "tools/call": return answer(await toToolResult(request.params, run));
1645
1800
  case "ping": return answer({});
1646
1801
  default:
@@ -1655,9 +1810,16 @@ async function toResponse(request, commands, run) {
1655
1810
  };
1656
1811
  }
1657
1812
  }
1813
+ /** The revisions this server speaks, newest first, which is what it may answer with. */
1814
+ const PROTOCOL_VERSIONS = [
1815
+ PROTOCOL_VERSION,
1816
+ "2025-03-26",
1817
+ "2024-11-05"
1818
+ ];
1819
+ /** toProtocolVersion answers a version this server implements, never one it was told. */
1658
1820
  function toProtocolVersion(params) {
1659
1821
  const asked = params?.["protocolVersion"];
1660
- return typeof asked === "string" ? asked : PROTOCOL_VERSION;
1822
+ return typeof asked === "string" && PROTOCOL_VERSIONS.includes(asked) ? asked : PROTOCOL_VERSION;
1661
1823
  }
1662
1824
  function toResource(uri, commands) {
1663
1825
  if (uri === GUIDE_URI) return { contents: [{
@@ -1670,10 +1832,6 @@ function toResource(uri, commands) {
1670
1832
  mimeType: "application/json",
1671
1833
  text: JSON.stringify(toTree(commands), null, 2)
1672
1834
  }] };
1673
- return {
1674
- contents: [],
1675
- isError: true
1676
- };
1677
1835
  }
1678
1836
  /** toTree names every command and what it takes, without the schemas a tool list would carry. */
1679
1837
  function toTree(commands) {
@@ -1691,12 +1849,32 @@ const REFUSED_COMMANDS = /* @__PURE__ */ new Set([
1691
1849
  "logout",
1692
1850
  "mcp"
1693
1851
  ]);
1852
+ /** The global options, which sit before a command and take a value of their own. */
1853
+ const GLOBAL_OPTIONS_WITH_VALUES = /* @__PURE__ */ new Set(["--api-url", "--issuer-url"]);
1854
+ /**
1855
+ * toCommandName reads which command a call names. A global option sits before it, so
1856
+ * reading the first argument alone would let --issuer-url x logout through.
1857
+ */
1858
+ function toCommandName(args) {
1859
+ for (let index = 0; index < args.length; index += 1) {
1860
+ const argument = args[index] ?? "";
1861
+ if (GLOBAL_OPTIONS_WITH_VALUES.has(argument)) {
1862
+ index += 1;
1863
+ continue;
1864
+ }
1865
+ if (argument.startsWith("-")) continue;
1866
+ return argument;
1867
+ }
1868
+ }
1869
+ /** toIsAskingForHelp reports whether a call only wants to read about a command. */
1870
+ function toIsAskingForHelp(args) {
1871
+ return args.includes("--help") || args.includes("-h") || args[0] === "help";
1872
+ }
1694
1873
  async function toToolResult(params, run) {
1695
- const name = params?.["name"];
1696
- if (name !== void 0 && name !== TOOL.name) return {
1874
+ if (params?.["name"] !== TOOL.name) return {
1697
1875
  content: [{
1698
1876
  type: "text",
1699
- text: `this server offers one tool, ${TOOL.name}`
1877
+ text: `a call names its tool, and this server offers one, ${TOOL.name}`
1700
1878
  }],
1701
1879
  isError: true
1702
1880
  };
@@ -1708,11 +1886,20 @@ async function toToolResult(params, run) {
1708
1886
  }],
1709
1887
  isError: true
1710
1888
  };
1889
+ const refused = toRefusedArgument(args);
1890
+ if (refused) return {
1891
+ content: [{
1892
+ type: "text",
1893
+ text: refused
1894
+ }],
1895
+ isError: true
1896
+ };
1711
1897
  const asked = args;
1712
- if (REFUSED_COMMANDS.has(asked[0] ?? "")) return {
1898
+ const command = toCommandName(asked) ?? "";
1899
+ if (REFUSED_COMMANDS.has(command) && !toIsAskingForHelp(asked)) return {
1713
1900
  content: [{
1714
1901
  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`
1902
+ text: `${command} is run by a person at a terminal, not through this server. Run hardfin ${command} yourself, then call this tool again`
1716
1903
  }],
1717
1904
  isError: true
1718
1905
  };
@@ -1725,13 +1912,14 @@ async function toToolResult(params, run) {
1725
1912
  isError: outcome.code !== 0
1726
1913
  };
1727
1914
  }
1728
- /** toRequestId reads the id of a line that could not be answered, so a client is not left waiting. */
1729
- function toRequestId(line) {
1730
- try {
1731
- return JSON.parse(line).id ?? null;
1732
- } catch {
1733
- return null;
1734
- }
1915
+ /** The most a command line can carry, below what the operating system refuses outright. */
1916
+ const ARGUMENT_LIMIT = 1e5;
1917
+ /** toRefusedArgument names an argument a command line cannot carry, rather than failing later. */
1918
+ function toRefusedArgument(args) {
1919
+ const withNul = args.findIndex((argument) => argument.includes("\0"));
1920
+ if (withNul >= 0) return `args[${withNul}] holds a NUL byte, which a command line cannot carry`;
1921
+ const total = args.reduce((carried, argument) => carried + argument.length, 0);
1922
+ if (total > ARGUMENT_LIMIT) return `these arguments are ${total} characters, and a command line carries at most ${ARGUMENT_LIMIT}`;
1735
1923
  }
1736
1924
  /** toCliRunner runs the CLI itself, so a tool call parses exactly as a terminal would. */
1737
1925
  function toCliRunner() {
@@ -1760,24 +1948,65 @@ function toCliRunner() {
1760
1948
  /** serve answers requests on stdin until the client closes it. */
1761
1949
  async function serve(commands, run = toCliRunner()) {
1762
1950
  const lines = createInterface({ input: process.stdin });
1951
+ const answering = /* @__PURE__ */ new Set();
1763
1952
  for await (const line of lines) {
1764
1953
  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
- };
1954
+ const answered = toAnswer(line, commands, run).finally(() => answering.delete(answered));
1955
+ answering.add(answered);
1956
+ }
1957
+ await Promise.all(answering);
1958
+ }
1959
+ /** toAnswer replies to one line, and never rejects, because a rejection ends the server. */
1960
+ async function toAnswer(line, commands, run) {
1961
+ let response;
1962
+ try {
1963
+ response = await toResponse(toRequest(line), commands, run);
1964
+ } catch (error) {
1965
+ response = error instanceof ProtocolFailure ? toErrorResponse(error.id, error.code, error.message) : toErrorResponse(toRequestId(line), -32603, error instanceof Error ? error.message : String(error));
1966
+ }
1967
+ if (response) write(`${JSON.stringify(response)}\n`);
1968
+ }
1969
+ /** toRequest reads one line, refusing what the protocol does not allow to be a request. */
1970
+ function toRequest(line) {
1971
+ let parsed;
1972
+ try {
1973
+ parsed = JSON.parse(line);
1974
+ } catch {
1975
+ throw new ProtocolFailure(-32700, "this line is not JSON", null);
1976
+ }
1977
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new ProtocolFailure(-32600, "a request is a JSON object, and this server takes one per line", null);
1978
+ const request = parsed;
1979
+ if (typeof request.method !== "string") throw new ProtocolFailure(-32600, "a request names a method", toAllowedId(request.id));
1980
+ if (request.id !== void 0 && request.id !== null && typeof request.id !== "string" && typeof request.id !== "number") throw new ProtocolFailure(-32600, "an id is a string, a number, or null", null);
1981
+ return request;
1982
+ }
1983
+ function toAllowedId(id) {
1984
+ return typeof id === "string" || typeof id === "number" ? id : null;
1985
+ }
1986
+ function toErrorResponse(id, code, message) {
1987
+ return {
1988
+ jsonrpc: "2.0",
1989
+ id,
1990
+ error: {
1991
+ code,
1992
+ message
1777
1993
  }
1778
- if (response) process.stdout.write(`${JSON.stringify(response)}\n`);
1994
+ };
1995
+ }
1996
+ /** toRequestId reads the id of a line that could not be answered, so a client is not left waiting. */
1997
+ function toRequestId(line) {
1998
+ try {
1999
+ return toAllowedId(JSON.parse(line).id);
2000
+ } catch {
2001
+ return null;
1779
2002
  }
1780
2003
  }
2004
+ /** write puts a reply on stdout, and stops quietly when the client has gone. */
2005
+ function write(text) {
2006
+ try {
2007
+ process.stdout.write(text);
2008
+ } catch {}
2009
+ }
1781
2010
  //#endregion
1782
2011
  //#region src/command/mcp.ts
1783
2012
  const mcpCommand = defineCommand({
@@ -1879,6 +2108,10 @@ async function runOperation(operation, input) {
1879
2108
  writeFailure(body.message, input.isJSON);
1880
2109
  return ExitCode.USAGE;
1881
2110
  }
2111
+ if (operation.bodyFlags.length > 0 && body === void 0) {
2112
+ writeFailure(`this command changes nothing unless a flag is given, such as ${operation.bodyFlags.slice(0, 3).map((flag) => `--${flag.name}`).join(", ")}. Run it with --help for the rest`, input.isJSON);
2113
+ return ExitCode.USAGE;
2114
+ }
1882
2115
  let form;
1883
2116
  if (operation.upload) {
1884
2117
  const built = await toForm(operation.upload, input.flags);
@@ -1908,7 +2141,7 @@ async function runOperation(operation, input) {
1908
2141
  return ExitCode.NOT_AUTHENTICATED;
1909
2142
  }
1910
2143
  if (error instanceof RequestFailure) {
1911
- writeFailure(error.message, input.isJSON, error.errors, error.requestId);
2144
+ writeFailure(error.message, input.isJSON, error.errors, error.requestId, error.status);
1912
2145
  return error.status === 401 ? ExitCode.NOT_AUTHENTICATED : ExitCode.ERROR;
1913
2146
  }
1914
2147
  writeFailure(error instanceof Error ? error.message : String(error), input.isJSON);
@@ -2021,10 +2254,11 @@ function toElements(flag, values) {
2021
2254
  const element = {};
2022
2255
  for (const pair of value.split(",")) {
2023
2256
  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);
2257
+ if (split < 1) return /* @__PURE__ */ new Error(`--${flag.name} takes ${flag.element?.join("=, ") ?? ""}=, such as --${flag.name} ${flag.element?.[0] ?? "field"}=value. A value cannot contain a comma`);
2258
+ const key = pair.slice(0, split).trim();
2026
2259
  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);
2260
+ if (key in element) return /* @__PURE__ */ new Error(`--${flag.name} names ${key} twice in one element, so which one it takes is not clear`);
2261
+ element[key] = pair.slice(split + 1).trim();
2028
2262
  }
2029
2263
  elements.push(element);
2030
2264
  }
@@ -2114,9 +2348,9 @@ const surfaceCommands = [
2114
2348
  name: "for-asset-id",
2115
2349
  queryName: "forAssetId",
2116
2350
  description: "The IDs of the assets to list",
2117
- valueName: "value",
2351
+ valueName: "uuid",
2118
2352
  repeatable: true,
2119
- schema: z.array(z.string())
2353
+ schema: z.array(z.uuid())
2120
2354
  },
2121
2355
  {
2122
2356
  name: "for-asset-key",
@@ -2130,33 +2364,33 @@ const surfaceCommands = [
2130
2364
  name: "for-customer",
2131
2365
  queryName: "forCustomer",
2132
2366
  description: "The IDs of the customers whose assets to list",
2133
- valueName: "value",
2367
+ valueName: "uuid",
2134
2368
  repeatable: true,
2135
- schema: z.array(z.string())
2369
+ schema: z.array(z.uuid())
2136
2370
  },
2137
2371
  {
2138
2372
  name: "for-item",
2139
2373
  queryName: "forItem",
2140
2374
  description: "The IDs of the items whose assets to list",
2141
- valueName: "value",
2375
+ valueName: "uuid",
2142
2376
  repeatable: true,
2143
- schema: z.array(z.string())
2377
+ schema: z.array(z.uuid())
2144
2378
  },
2145
2379
  {
2146
2380
  name: "at-site",
2147
2381
  queryName: "atSite",
2148
2382
  description: "The IDs of the locations whose assets to list",
2149
- valueName: "value",
2383
+ valueName: "uuid",
2150
2384
  repeatable: true,
2151
- schema: z.array(z.string())
2385
+ schema: z.array(z.uuid())
2152
2386
  },
2153
2387
  {
2154
2388
  name: "at-customer-sites",
2155
2389
  queryName: "atCustomerSites",
2156
2390
  description: "The IDs of the customers whose sites to list assets at",
2157
- valueName: "value",
2391
+ valueName: "uuid",
2158
2392
  repeatable: true,
2159
- schema: z.array(z.string())
2393
+ schema: z.array(z.uuid())
2160
2394
  },
2161
2395
  {
2162
2396
  name: "with-functional-statuses",
@@ -2220,9 +2454,9 @@ const surfaceCommands = [
2220
2454
  name: "for-project",
2221
2455
  queryName: "forProject",
2222
2456
  description: "The IDs of the projects whose assets to list",
2223
- valueName: "value",
2457
+ valueName: "uuid",
2224
2458
  repeatable: true,
2225
- schema: z.array(z.string())
2459
+ schema: z.array(z.uuid())
2226
2460
  },
2227
2461
  {
2228
2462
  name: "scrapped",
@@ -2241,7 +2475,7 @@ const surfaceCommands = [
2241
2475
  defineOperation({
2242
2476
  name: "create",
2243
2477
  summary: "Create asset",
2244
- example: "hardfin asset create --item-id <value> --serial <value>",
2478
+ example: "hardfin asset create --item-id <uuid> --serial <value>",
2245
2479
  method: "POST",
2246
2480
  path: "/asset",
2247
2481
  pathParameters: [],
@@ -2355,16 +2589,16 @@ const surfaceCommands = [
2355
2589
  name: "item-id",
2356
2590
  jsonPath: ["itemId"],
2357
2591
  description: "The ID of the catalog item the asset is a unit of",
2358
- valueName: "value",
2592
+ valueName: "uuid",
2359
2593
  required: true,
2360
- schema: z.string()
2594
+ schema: z.uuid()
2361
2595
  },
2362
2596
  {
2363
2597
  name: "location-id",
2364
2598
  jsonPath: ["locationId"],
2365
2599
  description: "The ID of the location the asset enters inventory at",
2366
- valueName: "value",
2367
- schema: z.string()
2600
+ valueName: "uuid",
2601
+ schema: z.uuid()
2368
2602
  },
2369
2603
  {
2370
2604
  name: "salvage-value",
@@ -2431,7 +2665,7 @@ const surfaceCommands = [
2431
2665
  subcommands: [defineOperation({
2432
2666
  name: "create",
2433
2667
  summary: "Execute asset move",
2434
- example: "hardfin asset move execute create",
2668
+ example: "hardfin asset move execute create --move assetId=<value>,deliverAt=<value>,deliverAtTimezone=<value>,destinationId=<value>,originId=<value>,shipAt=<value>,shipAtTimezone=<value>",
2435
2669
  method: "POST",
2436
2670
  path: "/asset/move/execute",
2437
2671
  pathParameters: [],
@@ -2440,7 +2674,7 @@ const surfaceCommands = [
2440
2674
  name: "move",
2441
2675
  jsonPath: ["moves"],
2442
2676
  description: "The moves to carry out",
2443
- valueName: "assetId=,deliverAt=",
2677
+ valueName: "assetId=,deliverAt=,deliverAtTimezone=,destinationId=,originId=,shipAt=,shipAtTimezone=",
2444
2678
  repeatable: true,
2445
2679
  element: [
2446
2680
  "assetId",
@@ -2463,7 +2697,7 @@ const surfaceCommands = [
2463
2697
  subcommands: [defineOperation({
2464
2698
  name: "create",
2465
2699
  summary: "Plan asset move",
2466
- example: "hardfin asset move plan create",
2700
+ example: "hardfin asset move plan create --move assetId=<value>,deliverAt=<value>,id=<value>,shipAt=<value>",
2467
2701
  method: "POST",
2468
2702
  path: "/asset/move/plan",
2469
2703
  pathParameters: [],
@@ -2472,7 +2706,7 @@ const surfaceCommands = [
2472
2706
  name: "move",
2473
2707
  jsonPath: ["moves"],
2474
2708
  description: "The moves to plan",
2475
- valueName: "assetId=,deliverAt=",
2709
+ valueName: "assetId=,deliverAt=,id=,shipAt=",
2476
2710
  repeatable: true,
2477
2711
  element: [
2478
2712
  "assetId",
@@ -2502,7 +2736,7 @@ const surfaceCommands = [
2502
2736
  defineOperation({
2503
2737
  name: "update",
2504
2738
  summary: "Patch asset",
2505
- example: "hardfin asset update ast_4f9xk2mq7plr8stz",
2739
+ example: "hardfin asset update ast_4f9xk2mq7plr8stz --metadata fieldId=<value>,value=<value>",
2506
2740
  method: "PATCH",
2507
2741
  path: "/asset/{assetKey}",
2508
2742
  pathParameters: [{
@@ -2545,9 +2779,9 @@ const surfaceCommands = [
2545
2779
  name: "initial-location-id",
2546
2780
  jsonPath: ["initialLocationId"],
2547
2781
  description: "The ID of the location the asset entered inventory at",
2548
- valueName: "value",
2782
+ valueName: "uuid",
2549
2783
  nullable: true,
2550
- schema: z.string()
2784
+ schema: z.uuid()
2551
2785
  },
2552
2786
  {
2553
2787
  name: "metadata",
@@ -2967,7 +3201,7 @@ const surfaceCommands = [
2967
3201
  defineOperation({
2968
3202
  name: "create",
2969
3203
  summary: "Create asset ownership",
2970
- example: "hardfin asset ownership create ast_4f9xk2mq7plr8stz --customer-id <value> --date <value>",
3204
+ example: "hardfin asset ownership create ast_4f9xk2mq7plr8stz --customer-id <uuid> --date <value>",
2971
3205
  method: "POST",
2972
3206
  path: "/asset/{assetKey}/ownership",
2973
3207
  pathParameters: [{
@@ -2981,9 +3215,9 @@ const surfaceCommands = [
2981
3215
  name: "customer-id",
2982
3216
  jsonPath: ["customerId"],
2983
3217
  description: "The ID of the customer that takes ownership of the asset",
2984
- valueName: "value",
3218
+ valueName: "uuid",
2985
3219
  required: true,
2986
- schema: z.string()
3220
+ schema: z.uuid()
2987
3221
  },
2988
3222
  {
2989
3223
  name: "date",
@@ -3063,9 +3297,9 @@ const surfaceCommands = [
3063
3297
  name: "customer-id",
3064
3298
  jsonPath: ["customerId"],
3065
3299
  description: "The ID of the customer that owned the asset during the segment",
3066
- valueName: "value",
3300
+ valueName: "uuid",
3067
3301
  nullable: true,
3068
- schema: z.string()
3302
+ schema: z.uuid()
3069
3303
  },
3070
3304
  {
3071
3305
  name: "date",
@@ -3585,7 +3819,7 @@ const surfaceCommands = [
3585
3819
  subcommands: [defineOperation({
3586
3820
  name: "create",
3587
3821
  summary: "Upload file",
3588
- example: "hardfin file create --file-type <value> --for-entity <value> --file photo.jpg",
3822
+ example: "hardfin file create --file-type <value> --for-entity <uuid> --file photo.jpg",
3589
3823
  method: "POST",
3590
3824
  path: "/file",
3591
3825
  pathParameters: [],
@@ -3606,9 +3840,9 @@ const surfaceCommands = [
3606
3840
  name: "for-entity",
3607
3841
  jsonPath: ["forEntity"],
3608
3842
  description: "The ID of the asset the file is attached to",
3609
- valueName: "value",
3843
+ valueName: "uuid",
3610
3844
  required: true,
3611
- schema: z.string()
3845
+ schema: z.uuid()
3612
3846
  },
3613
3847
  {
3614
3848
  name: "is-public",
@@ -3862,7 +4096,7 @@ const surfaceCommands = [
3862
4096
  defineOperation({
3863
4097
  name: "update",
3864
4098
  summary: "Update item",
3865
- example: "hardfin item update item_7Hq2Lm9XcR4tWz8K",
4099
+ example: "hardfin item update item_7Hq2Lm9XcR4tWz8K --field fieldId=<value>,order=<value>,section=<value>",
3866
4100
  method: "PATCH",
3867
4101
  path: "/item/{itemKey}",
3868
4102
  pathParameters: [{
@@ -3892,7 +4126,7 @@ const surfaceCommands = [
3892
4126
  name: "field",
3893
4127
  jsonPath: ["fields"],
3894
4128
  description: "New positions for a DEVICE item's fields",
3895
- valueName: "fieldId=,order=",
4129
+ valueName: "fieldId=,order=,section=",
3896
4130
  repeatable: true,
3897
4131
  element: [
3898
4132
  "fieldId",
@@ -4351,17 +4585,17 @@ const surfaceCommands = [
4351
4585
  name: "consignee",
4352
4586
  jsonPath: ["consignee"],
4353
4587
  description: "The ID of the customer a zone is designated for, such as for reservations, provisioning or a 3PL",
4354
- valueName: "value",
4588
+ valueName: "uuid",
4355
4589
  nullable: true,
4356
- schema: z.string()
4590
+ schema: z.uuid()
4357
4591
  },
4358
4592
  {
4359
4593
  name: "customer-id",
4360
4594
  jsonPath: ["customerId"],
4361
4595
  description: "The ID of the customer to assign a site to, or null for your organization's own site",
4362
- valueName: "value",
4596
+ valueName: "uuid",
4363
4597
  nullable: true,
4364
- schema: z.string()
4598
+ schema: z.uuid()
4365
4599
  },
4366
4600
  {
4367
4601
  name: "description",
@@ -4403,9 +4637,9 @@ const surfaceCommands = [
4403
4637
  name: "parent-location-id",
4404
4638
  jsonPath: ["parentLocationId"],
4405
4639
  description: "The ID of the site a zone belongs to, required for a zone and refused for a site",
4406
- valueName: "value",
4640
+ valueName: "uuid",
4407
4641
  nullable: true,
4408
- schema: z.string()
4642
+ schema: z.uuid()
4409
4643
  },
4410
4644
  {
4411
4645
  name: "type",
@@ -4507,17 +4741,17 @@ const surfaceCommands = [
4507
4741
  name: "consignee",
4508
4742
  jsonPath: ["consignee"],
4509
4743
  description: "The ID of the customer a zone is designated for, such as for reservations, provisioning or a 3PL",
4510
- valueName: "value",
4744
+ valueName: "uuid",
4511
4745
  nullable: true,
4512
- schema: z.string()
4746
+ schema: z.uuid()
4513
4747
  },
4514
4748
  {
4515
4749
  name: "customer-id",
4516
4750
  jsonPath: ["customerId"],
4517
4751
  description: "The ID of the customer to assign a site to, or null for your organization's own site",
4518
- valueName: "value",
4752
+ valueName: "uuid",
4519
4753
  nullable: true,
4520
- schema: z.string()
4754
+ schema: z.uuid()
4521
4755
  },
4522
4756
  {
4523
4757
  name: "description",
@@ -5030,6 +5264,10 @@ function collect(value, previous) {
5030
5264
  }
5031
5265
  //#endregion
5032
5266
  //#region src/cli.ts
5267
+ process.stdout.on("error", (error) => {
5268
+ if (error.code === "EPIPE") process.exit(0);
5269
+ throw error;
5270
+ });
5033
5271
  await toCli().parseAsync(process.argv);
5034
5272
  //#endregion
5035
5273
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardfin/cli",
3
- "version": "0.1.0-dev.19",
3
+ "version": "0.1.0-dev.21",
4
4
  "description": "Command line interface for the Hardfin API",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hardfin, Inc.",