@hardfin/cli 0.0.2-dev.5 → 0.0.2-dev.7

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 +113 -1
  2. package/dist/cli.js +1313 -18
  3. package/package.json +4 -2
package/dist/cli.js CHANGED
@@ -2,7 +2,8 @@
2
2
  import { createRequire } from "node:module";
3
3
  import { Command, Option } from "commander";
4
4
  import { z } from "zod";
5
- import { readFileSync } from "node:fs";
5
+ import { existsSync, readFileSync } from "node:fs";
6
+ import { resolve } from "node:path";
6
7
  //#region src/command/registry.ts
7
8
  /** ExitCode is what the process returns, and what an agent branches on. */
8
9
  const ExitCode = {
@@ -19,14 +20,93 @@ function defineCommand(command) {
19
20
  //#region src/config/settings.ts
20
21
  /** The API version this build was written against, sent on every request. */
21
22
  const API_VERSION = "2026-09-17";
23
+ /** The file a local build reads its overrides from, in the working directory. */
24
+ const CONFIG_FILE = "config.local.json";
22
25
  const DEFAULT_API_URL = "https://api.hardfin.com/v2";
23
- /** toApiUrl picks the API this invocation talks to. */
24
- function toApiUrl(override) {
25
- return (override ?? process.env["HARDFIN_API_URL"] ?? DEFAULT_API_URL).replace(/\/+$/, "");
26
+ const DEFAULT_ENV_FILE = ".env";
27
+ const FileSettings = z.strictObject({
28
+ apiUrl: z.string().optional(),
29
+ apiKey: z.string().optional(),
30
+ clientId: z.string().optional(),
31
+ auth: z.strictObject({
32
+ authorizeUrl: z.string().optional(),
33
+ tokenUrl: z.string().optional(),
34
+ deviceUrl: z.string().optional(),
35
+ revokeUrl: z.string().optional()
36
+ }).optional()
37
+ });
38
+ /** ConfigFailure is a config file that cannot be read or does not match the schema. */
39
+ var ConfigFailure = class extends Error {
40
+ constructor(message) {
41
+ super(message);
42
+ this.name = "ConfigFailure";
43
+ }
44
+ };
45
+ /** toSettings resolves what this invocation talks to, and where each value came from. */
46
+ function toSettings(flags = {}, directory = process.cwd()) {
47
+ const fromEnvFile = loadEnvFile(directory);
48
+ const file = toFileSettings(directory);
49
+ const sources = {};
50
+ const pick = (key, flag, variable, fromFile, fallback) => {
51
+ const [value, source] = toLayer(flag, process.env[variable], fromEnvFile.has(variable), fromFile, fallback);
52
+ sources[key] = source;
53
+ return value;
54
+ };
55
+ const apiUrl = toTrimmedUrl(pick("apiUrl", flags.apiUrl, "HARDFIN_API_URL", file.apiUrl, DEFAULT_API_URL) ?? DEFAULT_API_URL);
56
+ return {
57
+ settings: {
58
+ apiUrl,
59
+ apiKey: pick("apiKey", flags.apiKey, "HARDFIN_API_KEY", file.apiKey),
60
+ clientId: pick("clientId", flags.clientId, "HARDFIN_CLIENT_ID", file.clientId),
61
+ authorizeUrl: pick("authorizeUrl", flags.authorizeUrl, "HARDFIN_AUTHORIZE_URL", file.auth?.authorizeUrl, `${apiUrl}/auth/authorize`) ?? "",
62
+ tokenUrl: pick("tokenUrl", flags.tokenUrl, "HARDFIN_TOKEN_URL", file.auth?.tokenUrl, `${apiUrl}/auth/token`) ?? "",
63
+ deviceUrl: pick("deviceUrl", flags.deviceUrl, "HARDFIN_DEVICE_URL", file.auth?.deviceUrl, `${apiUrl}/auth/device`) ?? "",
64
+ revokeUrl: pick("revokeUrl", flags.revokeUrl, "HARDFIN_REVOKE_URL", file.auth?.revokeUrl, `${apiUrl}/auth/revoke`) ?? ""
65
+ },
66
+ sources
67
+ };
68
+ }
69
+ function toLayer(flag, environment, isFromEnvFile, file, fallback) {
70
+ if (flag) return [flag, "flag"];
71
+ if (environment) return [environment, isFromEnvFile ? "env file" : "environment"];
72
+ if (file) return [file, "config file"];
73
+ return [fallback, "default"];
74
+ }
75
+ /** toFileSettings reads the local override file, which a local build is expected to have. */
76
+ function toFileSettings(directory) {
77
+ const path = resolve(directory, CONFIG_FILE);
78
+ if (!existsSync(path)) return {};
79
+ let parsed;
80
+ try {
81
+ parsed = JSON.parse(readFileSync(path, "utf8"));
82
+ } catch {
83
+ throw new ConfigFailure(`${CONFIG_FILE} does not hold JSON`);
84
+ }
85
+ const result = FileSettings.safeParse(parsed);
86
+ if (!result.success) {
87
+ const issue = result.error.issues[0];
88
+ throw new ConfigFailure(`${CONFIG_FILE} is not valid: ${issue?.path.join(".") || "root"} ${issue?.message ?? ""}`.trim());
89
+ }
90
+ return result.data;
26
91
  }
27
- /** toApiKey reads the API key an unattended caller set. */
28
- function toApiKey() {
29
- return process.env["HARDFIN_API_KEY"] || void 0;
92
+ /**
93
+ * loadEnvFile reads a .env beside the command, so a local build needs no exports, and
94
+ * answers which variables it supplied. Node leaves an exported variable alone, so a
95
+ * shell export still wins over the file.
96
+ */
97
+ function loadEnvFile(directory) {
98
+ const path = process.env["HARDFIN_ENV_FILE"] ?? resolve(directory, DEFAULT_ENV_FILE);
99
+ if (!existsSync(path)) return /* @__PURE__ */ new Set();
100
+ const before = new Set(Object.keys(process.env));
101
+ try {
102
+ process.loadEnvFile(path);
103
+ } catch {
104
+ throw new ConfigFailure(`${path} cannot be read as an env file`);
105
+ }
106
+ return new Set(Object.keys(process.env).filter((name) => !before.has(name)));
107
+ }
108
+ function toTrimmedUrl(url) {
109
+ return url.replace(/\/+$/, "");
30
110
  }
31
111
  //#endregion
32
112
  //#region src/output/writer.ts
@@ -272,7 +352,7 @@ const apiCommand = defineCommand({
272
352
  run: runApi
273
353
  });
274
354
  async function runApi(input) {
275
- const apiKey = toApiKey();
355
+ const apiKey = input.resolved.settings.apiKey;
276
356
  if (!apiKey) {
277
357
  writeFailure("not authenticated. Set HARDFIN_API_KEY to an API key for your organization", input.isJSON);
278
358
  return ExitCode.NOT_AUTHENTICATED;
@@ -282,14 +362,14 @@ async function runApi(input) {
282
362
  writeFailure("a path is required, such as /customer", input.isJSON);
283
363
  return ExitCode.USAGE;
284
364
  }
285
- const query = toQuery(input.flags["field"]);
365
+ const query = toQuery$1(input.flags["field"]);
286
366
  if (query === void 0) {
287
367
  writeFailure("each --field is key=value, such as -f limit=50", input.isJSON);
288
368
  return ExitCode.USAGE;
289
369
  }
290
370
  let body;
291
371
  if (typeof input.flags["input"] === "string") {
292
- body = toBody(input.flags["input"]);
372
+ body = toBody$1(input.flags["input"]);
293
373
  if (body === void 0) {
294
374
  writeFailure(`${input.flags["input"]} does not hold JSON`, input.isJSON);
295
375
  return ExitCode.USAGE;
@@ -297,7 +377,7 @@ async function runApi(input) {
297
377
  }
298
378
  try {
299
379
  writeData((await request({
300
- apiUrl: toApiUrl(),
380
+ apiUrl: input.resolved.settings.apiUrl,
301
381
  apiKey,
302
382
  method: String(input.flags["method"] ?? "GET").toUpperCase(),
303
383
  path,
@@ -315,7 +395,7 @@ async function runApi(input) {
315
395
  }
316
396
  }
317
397
  /** toQuery folds the repeated --field flags into query parameters. */
318
- function toQuery(fields) {
398
+ function toQuery$1(fields) {
319
399
  const query = new URLSearchParams();
320
400
  for (const field of Array.isArray(fields) ? fields : []) {
321
401
  const split = field.indexOf("=");
@@ -324,6 +404,150 @@ function toQuery(fields) {
324
404
  }
325
405
  return query;
326
406
  }
407
+ function toBody$1(source) {
408
+ const text = source === "-" ? readFileSync(0, "utf8") : readFileSync(source, "utf8");
409
+ try {
410
+ return JSON.parse(text);
411
+ } catch {
412
+ return;
413
+ }
414
+ }
415
+ //#endregion
416
+ //#region src/command/config.ts
417
+ const configCommand = defineCommand({
418
+ name: "config",
419
+ summary: "Print what this invocation talks to, and where each value came from",
420
+ description: `Resolves the API and authentication endpoints from the flags, the environment, a .env file, and ${CONFIG_FILE} in the working directory. Use it when a local build reaches the wrong server.`,
421
+ arguments: [],
422
+ flags: [{
423
+ name: "json",
424
+ description: "Print machine-readable output, which is the default when stdout is not a terminal",
425
+ schema: z.boolean()
426
+ }],
427
+ examples: [{
428
+ description: "See what a local build is pointed at",
429
+ command: "hardfin config"
430
+ }, {
431
+ description: "Read one value from a script",
432
+ command: "hardfin config --json | jq -r .apiUrl"
433
+ }],
434
+ run: runConfig
435
+ });
436
+ /** toReport pairs each setting with the layer that supplied it. */
437
+ function toReport(resolved) {
438
+ const report = { apiVersion: API_VERSION };
439
+ for (const [key, value] of Object.entries(resolved.settings)) {
440
+ const source = resolved.sources[key];
441
+ report[key] = key === "apiKey" ? {
442
+ set: value !== void 0,
443
+ from: source
444
+ } : {
445
+ value: value ?? null,
446
+ from: source
447
+ };
448
+ }
449
+ return report;
450
+ }
451
+ async function runConfig(input) {
452
+ const report = toReport(input.resolved);
453
+ if (input.isJSON) {
454
+ writeData(report);
455
+ return ExitCode.OK;
456
+ }
457
+ writeData(Object.entries(report).map(([key, entry]) => {
458
+ if (typeof entry !== "object" || entry === null) return `${key.padEnd(14)} ${String(entry)}`;
459
+ const holder = entry;
460
+ const shown = holder.value ?? (holder.set ? "set" : "not set");
461
+ return `${key.padEnd(14)} ${shown} (${holder.from})`;
462
+ }).join("\n"));
463
+ return ExitCode.OK;
464
+ }
465
+ //#endregion
466
+ //#region src/command/operation.ts
467
+ const INPUT_FLAG = {
468
+ name: "input",
469
+ description: "A file holding the JSON request body, or - for stdin",
470
+ valueName: "file",
471
+ schema: z.string()
472
+ };
473
+ const JSON_FLAG = {
474
+ name: "json",
475
+ description: "Print machine-readable output, which is the default when stdout is not a terminal",
476
+ schema: z.boolean()
477
+ };
478
+ /** defineOperation turns one endpoint into the command that calls it. */
479
+ function defineOperation(operation) {
480
+ const flags = [...operation.queryFlags, JSON_FLAG];
481
+ if (operation.takesBody) flags.splice(flags.length - 1, 0, INPUT_FLAG);
482
+ return {
483
+ name: operation.name,
484
+ summary: operation.summary,
485
+ description: operation.description ?? operation.summary,
486
+ arguments: operation.pathParameters,
487
+ flags,
488
+ examples: [],
489
+ run: (input) => runOperation(operation, input)
490
+ };
491
+ }
492
+ async function runOperation(operation, input) {
493
+ const apiKey = input.resolved.settings.apiKey;
494
+ if (!apiKey) {
495
+ writeFailure("not authenticated. Set HARDFIN_API_KEY to an API key for your organization", input.isJSON);
496
+ return ExitCode.NOT_AUTHENTICATED;
497
+ }
498
+ const path = toPath(operation, input.args);
499
+ if (path === void 0) {
500
+ writeFailure(`this command takes ${operation.pathParameters.length} argument(s)`, input.isJSON);
501
+ return ExitCode.USAGE;
502
+ }
503
+ let body;
504
+ if (typeof input.flags["input"] === "string") {
505
+ body = toBody(input.flags["input"]);
506
+ if (body === void 0) {
507
+ writeFailure(`${input.flags["input"]} does not hold JSON`, input.isJSON);
508
+ return ExitCode.USAGE;
509
+ }
510
+ }
511
+ try {
512
+ writeData((await request({
513
+ apiUrl: input.resolved.settings.apiUrl,
514
+ apiKey,
515
+ method: operation.method,
516
+ path,
517
+ query: toQuery(operation, input.flags),
518
+ body
519
+ })).data);
520
+ return ExitCode.OK;
521
+ } catch (error) {
522
+ if (error instanceof RequestFailure) {
523
+ writeFailure(error.message, input.isJSON, error.errors, error.requestId);
524
+ return error.status === 401 ? ExitCode.NOT_AUTHENTICATED : ExitCode.ERROR;
525
+ }
526
+ writeFailure(error instanceof Error ? error.message : String(error), input.isJSON);
527
+ return ExitCode.ERROR;
528
+ }
529
+ }
530
+ /** toPath fills the path template from the positional arguments, in order. */
531
+ function toPath(operation, args) {
532
+ if (args.length !== operation.pathParameters.length) return;
533
+ let path = operation.path;
534
+ for (const [index, parameter] of operation.pathParameters.entries()) path = path.replace(`{${parameter.name}}`, encodeURIComponent(args[index] ?? ""));
535
+ return path;
536
+ }
537
+ /** toQuery carries only the flags this invocation actually set. */
538
+ function toQuery(operation, flags) {
539
+ const query = new URLSearchParams();
540
+ for (const flag of operation.queryFlags) {
541
+ const value = flags[toOptionKey(flag.name)];
542
+ if (value === void 0) continue;
543
+ for (const entry of Array.isArray(value) ? value : [value]) query.append(flag.queryName, String(entry));
544
+ }
545
+ return query;
546
+ }
547
+ /** toOptionKey names the parsed flag, which the parser reports in camel case. */
548
+ function toOptionKey(name) {
549
+ return name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
550
+ }
327
551
  function toBody(source) {
328
552
  const text = source === "-" ? readFileSync(0, "utf8") : readFileSync(source, "utf8");
329
553
  try {
@@ -335,11 +559,1073 @@ function toBody(source) {
335
559
  //#endregion
336
560
  //#region src/command/commands.ts
337
561
  /** commands is every command the CLI offers, and drives help and the agent guide. */
338
- const commands = [apiCommand, agentGuideCommand];
562
+ const commands = [
563
+ ...[
564
+ {
565
+ name: "asset",
566
+ summary: "Asset commands",
567
+ arguments: [],
568
+ flags: [],
569
+ examples: [],
570
+ subcommands: [
571
+ defineOperation({
572
+ name: "list",
573
+ summary: "Get asset listing",
574
+ method: "GET",
575
+ path: "/asset",
576
+ pathParameters: [],
577
+ queryFlags: [
578
+ {
579
+ name: "page",
580
+ queryName: "page",
581
+ description: "The page to return, starting at 1",
582
+ valueName: "number",
583
+ schema: z.coerce.number()
584
+ },
585
+ {
586
+ name: "limit",
587
+ queryName: "limit",
588
+ description: "The number of records per page, from 1 to 100",
589
+ valueName: "number",
590
+ schema: z.coerce.number()
591
+ },
592
+ {
593
+ name: "sort-by",
594
+ queryName: "sortBy",
595
+ description: "The field to sort by",
596
+ valueName: "value",
597
+ schema: z.enum([
598
+ "serial",
599
+ "project",
600
+ "item",
601
+ "location",
602
+ "owner",
603
+ "activity"
604
+ ])
605
+ },
606
+ {
607
+ name: "sort-order",
608
+ queryName: "sortOrder",
609
+ description: "The sort direction",
610
+ valueName: "value",
611
+ schema: z.enum(["ASC", "DESC"])
612
+ },
613
+ {
614
+ name: "archived",
615
+ queryName: "archived",
616
+ description: "Whether to return unarchived records, archived records, or all of them",
617
+ valueName: "value",
618
+ schema: z.enum([
619
+ "all",
620
+ "false",
621
+ "true"
622
+ ])
623
+ },
624
+ {
625
+ name: "search-query",
626
+ queryName: "searchQuery",
627
+ description: "Text to match against the serial, description, item, project, owner, and location, ignored when shorter than three characters",
628
+ valueName: "value",
629
+ schema: z.string()
630
+ },
631
+ {
632
+ name: "for-asset-id",
633
+ queryName: "forAssetId",
634
+ description: "The IDs of the assets to list",
635
+ valueName: "value",
636
+ repeatable: true,
637
+ schema: z.array(z.string())
638
+ },
639
+ {
640
+ name: "for-asset-key",
641
+ queryName: "forAssetKey",
642
+ description: "The keys of the assets to list",
643
+ valueName: "value",
644
+ repeatable: true,
645
+ schema: z.array(z.string())
646
+ },
647
+ {
648
+ name: "for-customer",
649
+ queryName: "forCustomer",
650
+ description: "The IDs of the customers whose assets to list",
651
+ valueName: "value",
652
+ repeatable: true,
653
+ schema: z.array(z.string())
654
+ },
655
+ {
656
+ name: "for-item",
657
+ queryName: "forItem",
658
+ description: "The IDs of the items whose assets to list",
659
+ valueName: "value",
660
+ repeatable: true,
661
+ schema: z.array(z.string())
662
+ },
663
+ {
664
+ name: "at-site",
665
+ queryName: "atSite",
666
+ description: "The IDs of the locations whose assets to list",
667
+ valueName: "value",
668
+ repeatable: true,
669
+ schema: z.array(z.string())
670
+ },
671
+ {
672
+ name: "at-customer-sites",
673
+ queryName: "atCustomerSites",
674
+ description: "The IDs of the customers whose sites to list assets at",
675
+ valueName: "value",
676
+ repeatable: true,
677
+ schema: z.array(z.string())
678
+ },
679
+ {
680
+ name: "with-functional-statuses",
681
+ queryName: "withFunctionalStatuses",
682
+ description: "The functional statuses to list, where SCRAPPED lists scrapped assets",
683
+ valueName: "value",
684
+ repeatable: true,
685
+ schema: z.array(z.enum([
686
+ "FUNCTIONAL",
687
+ "NEEDS_REVIEW",
688
+ "NON-FUNCTIONAL",
689
+ "SCRAPPED"
690
+ ]))
691
+ },
692
+ {
693
+ name: "with-transit-statuses",
694
+ queryName: "withTransitStatuses",
695
+ description: "The transit statuses to list",
696
+ valueName: "value",
697
+ repeatable: true,
698
+ schema: z.array(z.enum([
699
+ "IN_TRANSIT",
700
+ "IN_TRANSIT_TO_FIELD",
701
+ "IN_TRANSIT_TO_INVENTORY",
702
+ "NOT_IN_TRANSIT"
703
+ ]))
704
+ },
705
+ {
706
+ name: "with-inventory-statuses",
707
+ queryName: "withInventoryStatuses",
708
+ description: "In_inventory, not_in_inventory, or both, which filters only when one is sent",
709
+ valueName: "value",
710
+ repeatable: true,
711
+ schema: z.array(z.string())
712
+ },
713
+ {
714
+ name: "with-location-company-type",
715
+ queryName: "withLocationCompanyType",
716
+ description: "Customer, manufacturer, or both, for assets at customer sites or your own, which filters only when one is sent",
717
+ valueName: "value",
718
+ repeatable: true,
719
+ schema: z.array(z.string())
720
+ },
721
+ {
722
+ name: "with-project-status",
723
+ queryName: "withProjectStatus",
724
+ description: "The project assignments to list: upcoming, active, past, or none",
725
+ valueName: "value",
726
+ repeatable: true,
727
+ schema: z.array(z.string())
728
+ },
729
+ {
730
+ name: "with-owner",
731
+ queryName: "withOwner",
732
+ description: "Manufacturer for assets your organization owns, customer for assets customers own, or both",
733
+ valueName: "value",
734
+ repeatable: true,
735
+ schema: z.array(z.string())
736
+ },
737
+ {
738
+ name: "for-project",
739
+ queryName: "forProject",
740
+ description: "The IDs of the projects whose assets to list",
741
+ valueName: "value",
742
+ repeatable: true,
743
+ schema: z.array(z.string())
744
+ },
745
+ {
746
+ name: "scrapped",
747
+ queryName: "scrapped",
748
+ description: "Whether to list unscrapped assets, scrapped assets, or all of them",
749
+ valueName: "value",
750
+ schema: z.enum([
751
+ "all",
752
+ "false",
753
+ "true"
754
+ ])
755
+ }
756
+ ],
757
+ takesBody: false
758
+ }),
759
+ defineOperation({
760
+ name: "create",
761
+ summary: "Create asset",
762
+ method: "POST",
763
+ path: "/asset",
764
+ pathParameters: [],
765
+ queryFlags: [],
766
+ takesBody: true
767
+ }),
768
+ defineOperation({
769
+ name: "get",
770
+ summary: "Get asset",
771
+ method: "GET",
772
+ path: "/asset/{assetKey}",
773
+ pathParameters: [{
774
+ name: "assetKey",
775
+ description: "The asset's key",
776
+ required: true
777
+ }],
778
+ queryFlags: [],
779
+ takesBody: false
780
+ }),
781
+ defineOperation({
782
+ name: "update",
783
+ summary: "Patch asset",
784
+ method: "PATCH",
785
+ path: "/asset/{assetKey}",
786
+ pathParameters: [{
787
+ name: "assetKey",
788
+ description: "The asset's key",
789
+ required: true
790
+ }],
791
+ queryFlags: [],
792
+ takesBody: true
793
+ }),
794
+ {
795
+ name: "accounting",
796
+ summary: "Accounting commands",
797
+ arguments: [],
798
+ flags: [],
799
+ examples: [],
800
+ subcommands: [defineOperation({
801
+ name: "update",
802
+ summary: "Update asset accounting",
803
+ method: "PATCH",
804
+ path: "/asset/{assetKey}/accounting",
805
+ pathParameters: [{
806
+ name: "assetKey",
807
+ description: "The asset's key",
808
+ required: true
809
+ }],
810
+ queryFlags: [],
811
+ takesBody: true
812
+ }), {
813
+ name: "in-service-management",
814
+ summary: "In service management commands",
815
+ arguments: [],
816
+ flags: [],
817
+ examples: [],
818
+ subcommands: [defineOperation({
819
+ name: "update",
820
+ summary: "Toggle in service date management",
821
+ method: "PATCH",
822
+ path: "/asset/{assetKey}/accounting/in-service-management",
823
+ pathParameters: [{
824
+ name: "assetKey",
825
+ description: "The asset's key",
826
+ required: true
827
+ }],
828
+ queryFlags: [],
829
+ takesBody: true
830
+ })]
831
+ }]
832
+ },
833
+ {
834
+ name: "cost-adjustment",
835
+ summary: "Cost adjustment commands",
836
+ arguments: [],
837
+ flags: [],
838
+ examples: [],
839
+ subcommands: [defineOperation({
840
+ name: "create",
841
+ summary: "Create asset cost adjustment",
842
+ method: "POST",
843
+ path: "/asset/{assetKey}/cost-adjustment",
844
+ pathParameters: [{
845
+ name: "assetKey",
846
+ description: "The asset's key",
847
+ required: true
848
+ }],
849
+ queryFlags: [],
850
+ takesBody: true
851
+ })]
852
+ },
853
+ {
854
+ name: "file",
855
+ summary: "File commands",
856
+ arguments: [],
857
+ flags: [],
858
+ examples: [],
859
+ subcommands: [defineOperation({
860
+ name: "list",
861
+ summary: "Get asset files",
862
+ method: "GET",
863
+ path: "/asset/{assetKey}/file",
864
+ pathParameters: [{
865
+ name: "assetKey",
866
+ description: "The asset's key",
867
+ required: true
868
+ }],
869
+ queryFlags: [],
870
+ takesBody: false
871
+ }), defineOperation({
872
+ name: "delete",
873
+ summary: "Delete asset file",
874
+ method: "DELETE",
875
+ path: "/asset/{assetKey}/file/{fileKey}",
876
+ pathParameters: [{
877
+ name: "assetKey",
878
+ description: "The asset's key",
879
+ required: true
880
+ }, {
881
+ name: "fileKey",
882
+ description: "The file's key",
883
+ required: true
884
+ }],
885
+ queryFlags: [],
886
+ takesBody: false
887
+ })]
888
+ },
889
+ {
890
+ name: "ownership",
891
+ summary: "Ownership commands",
892
+ arguments: [],
893
+ flags: [],
894
+ examples: [],
895
+ subcommands: [
896
+ defineOperation({
897
+ name: "list",
898
+ summary: "Get asset ownership history",
899
+ method: "GET",
900
+ path: "/asset/{assetKey}/ownership",
901
+ pathParameters: [{
902
+ name: "assetKey",
903
+ description: "The asset's key",
904
+ required: true
905
+ }],
906
+ queryFlags: [],
907
+ takesBody: false
908
+ }),
909
+ defineOperation({
910
+ name: "create",
911
+ summary: "Create asset ownership",
912
+ method: "POST",
913
+ path: "/asset/{assetKey}/ownership",
914
+ pathParameters: [{
915
+ name: "assetKey",
916
+ description: "The asset's key",
917
+ required: true
918
+ }],
919
+ queryFlags: [],
920
+ takesBody: true
921
+ }),
922
+ defineOperation({
923
+ name: "clear",
924
+ summary: "Clear the ownership an asset holds today",
925
+ method: "DELETE",
926
+ path: "/asset/{assetKey}/ownership",
927
+ pathParameters: [{
928
+ name: "assetKey",
929
+ description: "The asset's key",
930
+ required: true
931
+ }],
932
+ queryFlags: [],
933
+ takesBody: true
934
+ }),
935
+ defineOperation({
936
+ name: "get",
937
+ summary: "Get asset ownership segment",
938
+ method: "GET",
939
+ path: "/asset/{assetKey}/ownership/{segmentKey}",
940
+ pathParameters: [{
941
+ name: "assetKey",
942
+ description: "The asset's key",
943
+ required: true
944
+ }, {
945
+ name: "segmentKey",
946
+ description: "The ownership segment's key",
947
+ required: true
948
+ }],
949
+ queryFlags: [],
950
+ takesBody: false
951
+ }),
952
+ defineOperation({
953
+ name: "update",
954
+ summary: "Patch asset ownership segment",
955
+ method: "PATCH",
956
+ path: "/asset/{assetKey}/ownership/{segmentKey}",
957
+ pathParameters: [{
958
+ name: "assetKey",
959
+ description: "The asset's key",
960
+ required: true
961
+ }, {
962
+ name: "segmentKey",
963
+ description: "The ownership segment's key",
964
+ required: true
965
+ }],
966
+ queryFlags: [],
967
+ takesBody: true
968
+ }),
969
+ defineOperation({
970
+ name: "delete",
971
+ summary: "Delete asset ownership segment",
972
+ method: "DELETE",
973
+ path: "/asset/{assetKey}/ownership/{segmentKey}",
974
+ pathParameters: [{
975
+ name: "assetKey",
976
+ description: "The asset's key",
977
+ required: true
978
+ }, {
979
+ name: "segmentKey",
980
+ description: "The ownership segment's key",
981
+ required: true
982
+ }],
983
+ queryFlags: [],
984
+ takesBody: false
985
+ })
986
+ ]
987
+ },
988
+ {
989
+ name: "url-link",
990
+ summary: "URL link commands",
991
+ arguments: [],
992
+ flags: [],
993
+ examples: [],
994
+ subcommands: [defineOperation({
995
+ name: "list",
996
+ summary: "Get asset URL links",
997
+ method: "GET",
998
+ path: "/asset/{assetKey}/url-link",
999
+ pathParameters: [{
1000
+ name: "assetKey",
1001
+ description: "The asset's key",
1002
+ required: true
1003
+ }],
1004
+ queryFlags: [],
1005
+ takesBody: false
1006
+ }), defineOperation({
1007
+ name: "create",
1008
+ summary: "Create asset URL link",
1009
+ method: "POST",
1010
+ path: "/asset/{assetKey}/url-link",
1011
+ pathParameters: [{
1012
+ name: "assetKey",
1013
+ description: "The asset's key",
1014
+ required: true
1015
+ }],
1016
+ queryFlags: [],
1017
+ takesBody: true
1018
+ })]
1019
+ },
1020
+ {
1021
+ name: "useful-life-revision",
1022
+ summary: "Useful life revision commands",
1023
+ arguments: [],
1024
+ flags: [],
1025
+ examples: [],
1026
+ subcommands: [defineOperation({
1027
+ name: "create",
1028
+ summary: "Create asset useful life revision",
1029
+ method: "POST",
1030
+ path: "/asset/{assetKey}/useful-life-revision",
1031
+ pathParameters: [{
1032
+ name: "assetKey",
1033
+ description: "The asset's key",
1034
+ required: true
1035
+ }],
1036
+ queryFlags: [],
1037
+ takesBody: true
1038
+ })]
1039
+ }
1040
+ ]
1041
+ },
1042
+ {
1043
+ name: "customer",
1044
+ summary: "Customer commands",
1045
+ arguments: [],
1046
+ flags: [],
1047
+ examples: [],
1048
+ subcommands: [
1049
+ defineOperation({
1050
+ name: "list",
1051
+ summary: "Get customers",
1052
+ method: "GET",
1053
+ path: "/customer",
1054
+ pathParameters: [],
1055
+ queryFlags: [
1056
+ {
1057
+ name: "page",
1058
+ queryName: "page",
1059
+ description: "The page to return, starting at 1",
1060
+ valueName: "number",
1061
+ schema: z.coerce.number()
1062
+ },
1063
+ {
1064
+ name: "limit",
1065
+ queryName: "limit",
1066
+ description: "The number of records per page, from 1 to 100",
1067
+ valueName: "number",
1068
+ schema: z.coerce.number()
1069
+ },
1070
+ {
1071
+ name: "sort-by",
1072
+ queryName: "sortBy",
1073
+ description: "The field to sort by",
1074
+ valueName: "value",
1075
+ schema: z.string()
1076
+ },
1077
+ {
1078
+ name: "sort-order",
1079
+ queryName: "sortOrder",
1080
+ description: "The sort direction",
1081
+ valueName: "value",
1082
+ schema: z.enum(["ASC", "DESC"])
1083
+ },
1084
+ {
1085
+ name: "archived",
1086
+ queryName: "archived",
1087
+ description: "Whether to return unarchived records, archived records, or all of them",
1088
+ valueName: "value",
1089
+ schema: z.enum([
1090
+ "all",
1091
+ "false",
1092
+ "true"
1093
+ ])
1094
+ },
1095
+ {
1096
+ name: "is-customer",
1097
+ queryName: "isCustomer",
1098
+ description: "True to return only customers, or false to return only non-customers",
1099
+ schema: z.boolean()
1100
+ },
1101
+ {
1102
+ name: "is-supplier",
1103
+ queryName: "isSupplier",
1104
+ description: "True to return only suppliers, or false to return only non-suppliers",
1105
+ schema: z.boolean()
1106
+ },
1107
+ {
1108
+ name: "sync-statuses",
1109
+ queryName: "syncStatuses",
1110
+ description: "The CRM sync statuses to return: in-sync, out-of-sync, or not-linked",
1111
+ valueName: "value",
1112
+ repeatable: true,
1113
+ schema: z.array(z.string())
1114
+ },
1115
+ {
1116
+ name: "search",
1117
+ queryName: "search",
1118
+ description: "Text to match against customer names",
1119
+ valueName: "value",
1120
+ schema: z.string()
1121
+ }
1122
+ ],
1123
+ takesBody: false
1124
+ }),
1125
+ defineOperation({
1126
+ name: "create",
1127
+ summary: "Create customer",
1128
+ method: "POST",
1129
+ path: "/customer",
1130
+ pathParameters: [],
1131
+ queryFlags: [],
1132
+ takesBody: true
1133
+ }),
1134
+ defineOperation({
1135
+ name: "get",
1136
+ summary: "Get customer",
1137
+ method: "GET",
1138
+ path: "/customer/{customerKey}",
1139
+ pathParameters: [{
1140
+ name: "customerKey",
1141
+ description: "The customer's key",
1142
+ required: true
1143
+ }],
1144
+ queryFlags: [],
1145
+ takesBody: false
1146
+ }),
1147
+ defineOperation({
1148
+ name: "update",
1149
+ summary: "Patch customer",
1150
+ method: "PATCH",
1151
+ path: "/customer/{customerKey}",
1152
+ pathParameters: [{
1153
+ name: "customerKey",
1154
+ description: "The customer's key",
1155
+ required: true
1156
+ }],
1157
+ queryFlags: [],
1158
+ takesBody: true
1159
+ })
1160
+ ]
1161
+ },
1162
+ {
1163
+ name: "file",
1164
+ summary: "File commands",
1165
+ arguments: [],
1166
+ flags: [],
1167
+ examples: [],
1168
+ subcommands: [defineOperation({
1169
+ name: "create",
1170
+ summary: "Upload file",
1171
+ method: "POST",
1172
+ path: "/file",
1173
+ pathParameters: [],
1174
+ queryFlags: [],
1175
+ takesBody: true
1176
+ }), defineOperation({
1177
+ name: "get",
1178
+ summary: "Get file",
1179
+ method: "GET",
1180
+ path: "/file/{fileKey}",
1181
+ pathParameters: [{
1182
+ name: "fileKey",
1183
+ description: "The file's key, and a public file is readable with any organization's API key while any other file is readable only with its own organization's",
1184
+ required: true
1185
+ }],
1186
+ queryFlags: [{
1187
+ name: "attachment",
1188
+ queryName: "attachment",
1189
+ description: "Present, with any value or none, when the file should download as an attachment rather than open inline",
1190
+ valueName: "value",
1191
+ schema: z.string()
1192
+ }],
1193
+ takesBody: false
1194
+ })]
1195
+ },
1196
+ {
1197
+ name: "item",
1198
+ summary: "Item commands",
1199
+ arguments: [],
1200
+ flags: [],
1201
+ examples: [],
1202
+ subcommands: [
1203
+ defineOperation({
1204
+ name: "list",
1205
+ summary: "Get items",
1206
+ method: "GET",
1207
+ path: "/item",
1208
+ pathParameters: [],
1209
+ queryFlags: [
1210
+ {
1211
+ name: "type",
1212
+ queryName: "type",
1213
+ description: "The item type to list, SERVICE, DEVICE, or BULK, or absent for every type",
1214
+ valueName: "value",
1215
+ schema: z.enum([
1216
+ "BULK",
1217
+ "DEVICE",
1218
+ "SERVICE"
1219
+ ])
1220
+ },
1221
+ {
1222
+ name: "search",
1223
+ queryName: "search",
1224
+ description: "Text to match against item names, SKUs, and descriptions, in any case",
1225
+ valueName: "value",
1226
+ schema: z.string()
1227
+ },
1228
+ {
1229
+ name: "page",
1230
+ queryName: "page",
1231
+ description: "The page to return, starting at 1",
1232
+ valueName: "number",
1233
+ schema: z.coerce.number()
1234
+ },
1235
+ {
1236
+ name: "limit",
1237
+ queryName: "limit",
1238
+ description: "The number of records per page, from 1 to 100",
1239
+ valueName: "number",
1240
+ schema: z.coerce.number()
1241
+ },
1242
+ {
1243
+ name: "sort-by",
1244
+ queryName: "sortBy",
1245
+ description: "The field to sort by",
1246
+ valueName: "value",
1247
+ schema: z.enum([
1248
+ "name",
1249
+ "sku",
1250
+ "type",
1251
+ "lastUpdatedAt"
1252
+ ])
1253
+ },
1254
+ {
1255
+ name: "sort-order",
1256
+ queryName: "sortOrder",
1257
+ description: "The sort direction",
1258
+ valueName: "value",
1259
+ schema: z.enum(["ASC", "DESC"])
1260
+ },
1261
+ {
1262
+ name: "archived",
1263
+ queryName: "archived",
1264
+ description: "Whether to return unarchived records, archived records, or all of them",
1265
+ valueName: "value",
1266
+ schema: z.enum([
1267
+ "all",
1268
+ "false",
1269
+ "true"
1270
+ ])
1271
+ },
1272
+ {
1273
+ name: "exclude-linked-integration",
1274
+ queryName: "excludeLinkedIntegration",
1275
+ description: "An integration whose already-linked items to leave out",
1276
+ valueName: "value",
1277
+ schema: z.string()
1278
+ }
1279
+ ],
1280
+ takesBody: false
1281
+ }),
1282
+ defineOperation({
1283
+ name: "create",
1284
+ summary: "Create item",
1285
+ method: "POST",
1286
+ path: "/item",
1287
+ pathParameters: [],
1288
+ queryFlags: [],
1289
+ takesBody: true
1290
+ }),
1291
+ defineOperation({
1292
+ name: "get",
1293
+ summary: "Get item",
1294
+ method: "GET",
1295
+ path: "/item/{itemKey}",
1296
+ pathParameters: [{
1297
+ name: "itemKey",
1298
+ description: "The item's key",
1299
+ required: true
1300
+ }],
1301
+ queryFlags: [],
1302
+ takesBody: false
1303
+ }),
1304
+ defineOperation({
1305
+ name: "update",
1306
+ summary: "Update item",
1307
+ method: "PATCH",
1308
+ path: "/item/{itemKey}",
1309
+ pathParameters: [{
1310
+ name: "itemKey",
1311
+ description: "The item's key",
1312
+ required: true
1313
+ }],
1314
+ queryFlags: [],
1315
+ takesBody: true
1316
+ }),
1317
+ {
1318
+ name: "accounting",
1319
+ summary: "Accounting commands",
1320
+ arguments: [],
1321
+ flags: [],
1322
+ examples: [],
1323
+ subcommands: [defineOperation({
1324
+ name: "update",
1325
+ summary: "Update item accounting",
1326
+ method: "PATCH",
1327
+ path: "/item/{itemKey}/accounting",
1328
+ pathParameters: [{
1329
+ name: "itemKey",
1330
+ description: "The item's key",
1331
+ required: true
1332
+ }],
1333
+ queryFlags: [],
1334
+ takesBody: true
1335
+ })]
1336
+ },
1337
+ {
1338
+ name: "field",
1339
+ summary: "Field commands",
1340
+ arguments: [],
1341
+ flags: [],
1342
+ examples: [],
1343
+ subcommands: [
1344
+ defineOperation({
1345
+ name: "create",
1346
+ summary: "Create item field",
1347
+ method: "POST",
1348
+ path: "/item/{itemKey}/field",
1349
+ pathParameters: [{
1350
+ name: "itemKey",
1351
+ description: "The item's key",
1352
+ required: true
1353
+ }],
1354
+ queryFlags: [],
1355
+ takesBody: true
1356
+ }),
1357
+ defineOperation({
1358
+ name: "update",
1359
+ summary: "Update item field",
1360
+ method: "PATCH",
1361
+ path: "/item/{itemKey}/field/{fieldKey}",
1362
+ pathParameters: [{
1363
+ name: "itemKey",
1364
+ description: "The item's key",
1365
+ required: true
1366
+ }, {
1367
+ name: "fieldKey",
1368
+ description: "The field's key",
1369
+ required: true
1370
+ }],
1371
+ queryFlags: [],
1372
+ takesBody: true
1373
+ }),
1374
+ defineOperation({
1375
+ name: "delete",
1376
+ summary: "Delete item field",
1377
+ method: "DELETE",
1378
+ path: "/item/{itemKey}/field/{fieldKey}",
1379
+ pathParameters: [{
1380
+ name: "itemKey",
1381
+ description: "The item's key",
1382
+ required: true
1383
+ }, {
1384
+ name: "fieldKey",
1385
+ description: "The field's key",
1386
+ required: true
1387
+ }],
1388
+ queryFlags: [],
1389
+ takesBody: false
1390
+ })
1391
+ ]
1392
+ }
1393
+ ]
1394
+ },
1395
+ {
1396
+ name: "location",
1397
+ summary: "Location commands",
1398
+ arguments: [],
1399
+ flags: [],
1400
+ examples: [],
1401
+ subcommands: [
1402
+ defineOperation({
1403
+ name: "list",
1404
+ summary: "Get location listing",
1405
+ method: "GET",
1406
+ path: "/location",
1407
+ pathParameters: [],
1408
+ queryFlags: [
1409
+ {
1410
+ name: "page",
1411
+ queryName: "page",
1412
+ description: "The page to return, starting at 1",
1413
+ valueName: "number",
1414
+ schema: z.coerce.number()
1415
+ },
1416
+ {
1417
+ name: "limit",
1418
+ queryName: "limit",
1419
+ description: "The number of records per page, from 1 to 100",
1420
+ valueName: "number",
1421
+ schema: z.coerce.number()
1422
+ },
1423
+ {
1424
+ name: "sort-by",
1425
+ queryName: "sortBy",
1426
+ description: "The field to sort by",
1427
+ valueName: "value",
1428
+ schema: z.enum([
1429
+ "name",
1430
+ "company",
1431
+ "assetCount"
1432
+ ])
1433
+ },
1434
+ {
1435
+ name: "sort-order",
1436
+ queryName: "sortOrder",
1437
+ description: "The sort direction",
1438
+ valueName: "value",
1439
+ schema: z.enum(["ASC", "DESC"])
1440
+ },
1441
+ {
1442
+ name: "archived",
1443
+ queryName: "archived",
1444
+ description: "Whether to return unarchived records, archived records, or all of them",
1445
+ valueName: "value",
1446
+ schema: z.enum([
1447
+ "all",
1448
+ "false",
1449
+ "true"
1450
+ ])
1451
+ },
1452
+ {
1453
+ name: "is-transient",
1454
+ queryName: "isTransient",
1455
+ description: "Whether to return permanent locations (false), transient locations (true), or both (all)",
1456
+ valueName: "value",
1457
+ schema: z.enum([
1458
+ "all",
1459
+ "false",
1460
+ "true"
1461
+ ])
1462
+ },
1463
+ {
1464
+ name: "search",
1465
+ queryName: "search",
1466
+ description: "Text to match against location and customer names",
1467
+ valueName: "value",
1468
+ schema: z.string()
1469
+ },
1470
+ {
1471
+ name: "for-customer-ids",
1472
+ queryName: "forCustomerIds",
1473
+ description: "The IDs of the customers whose locations to return",
1474
+ valueName: "value",
1475
+ repeatable: true,
1476
+ schema: z.array(z.string())
1477
+ },
1478
+ {
1479
+ name: "include-organization",
1480
+ queryName: "includeOrganization",
1481
+ description: "Whether the customer filter also matches your organization's own locations, which on its own returns only those",
1482
+ schema: z.boolean()
1483
+ },
1484
+ {
1485
+ name: "sync-statuses",
1486
+ queryName: "syncStatuses",
1487
+ description: "The CRM sync statuses to return: in-sync, out-of-sync, or not-linked",
1488
+ valueName: "value",
1489
+ repeatable: true,
1490
+ schema: z.array(z.string())
1491
+ }
1492
+ ],
1493
+ takesBody: false
1494
+ }),
1495
+ defineOperation({
1496
+ name: "create",
1497
+ summary: "Create location",
1498
+ method: "POST",
1499
+ path: "/location",
1500
+ pathParameters: [],
1501
+ queryFlags: [],
1502
+ takesBody: true
1503
+ }),
1504
+ defineOperation({
1505
+ name: "get",
1506
+ summary: "Get location",
1507
+ method: "GET",
1508
+ path: "/location/{locationKey}",
1509
+ pathParameters: [{
1510
+ name: "locationKey",
1511
+ description: "The location's key",
1512
+ required: true
1513
+ }],
1514
+ queryFlags: [],
1515
+ takesBody: false
1516
+ }),
1517
+ defineOperation({
1518
+ name: "update",
1519
+ summary: "Patch location",
1520
+ method: "PATCH",
1521
+ path: "/location/{locationKey}",
1522
+ pathParameters: [{
1523
+ name: "locationKey",
1524
+ description: "The location's key",
1525
+ required: true
1526
+ }],
1527
+ queryFlags: [],
1528
+ takesBody: true
1529
+ }),
1530
+ {
1531
+ name: "zones",
1532
+ summary: "Zones commands",
1533
+ arguments: [],
1534
+ flags: [],
1535
+ examples: [],
1536
+ subcommands: [defineOperation({
1537
+ name: "list",
1538
+ summary: "Get zones",
1539
+ method: "GET",
1540
+ path: "/location/{locationKey}/zones",
1541
+ pathParameters: [{
1542
+ name: "locationKey",
1543
+ description: "The key of the site whose zones to list",
1544
+ required: true
1545
+ }],
1546
+ queryFlags: [{
1547
+ name: "archived",
1548
+ queryName: "archived",
1549
+ description: "Whether to return unarchived zones, archived zones, or all of them",
1550
+ valueName: "value",
1551
+ schema: z.enum([
1552
+ "all",
1553
+ "false",
1554
+ "true"
1555
+ ])
1556
+ }],
1557
+ takesBody: false
1558
+ })]
1559
+ }
1560
+ ]
1561
+ },
1562
+ {
1563
+ name: "url-link",
1564
+ summary: "URL link commands",
1565
+ arguments: [],
1566
+ flags: [],
1567
+ examples: [],
1568
+ subcommands: [
1569
+ defineOperation({
1570
+ name: "get",
1571
+ summary: "Get URL link by key",
1572
+ method: "GET",
1573
+ path: "/url-link/{linkKey}",
1574
+ pathParameters: [{
1575
+ name: "linkKey",
1576
+ description: "The URL link's key",
1577
+ required: true
1578
+ }],
1579
+ queryFlags: [],
1580
+ takesBody: false
1581
+ }),
1582
+ defineOperation({
1583
+ name: "update",
1584
+ summary: "Update URL link",
1585
+ method: "PATCH",
1586
+ path: "/url-link/{linkKey}",
1587
+ pathParameters: [{
1588
+ name: "linkKey",
1589
+ description: "The URL link's key",
1590
+ required: true
1591
+ }],
1592
+ queryFlags: [],
1593
+ takesBody: true
1594
+ }),
1595
+ defineOperation({
1596
+ name: "delete",
1597
+ summary: "Delete URL link",
1598
+ method: "DELETE",
1599
+ path: "/url-link/{linkKey}",
1600
+ pathParameters: [{
1601
+ name: "linkKey",
1602
+ description: "The URL link's key",
1603
+ required: true
1604
+ }],
1605
+ queryFlags: [],
1606
+ takesBody: false
1607
+ })
1608
+ ]
1609
+ }
1610
+ ],
1611
+ apiCommand,
1612
+ configCommand,
1613
+ agentGuideCommand
1614
+ ];
1615
+ //#endregion
1616
+ //#region src/command/validate.ts
1617
+ /** toRejectedFlag names the first flag whose value its schema refuses. */
1618
+ function toRejectedFlag(command, flags) {
1619
+ for (const flag of command.flags) {
1620
+ const value = flags[toOptionKey(flag.name)];
1621
+ if (value === void 0) continue;
1622
+ if (!flag.schema.safeParse(value).success) return `--${flag.name} does not accept ${JSON.stringify(value)}`;
1623
+ }
1624
+ }
339
1625
  //#endregion
340
1626
  //#region src/cli.ts
341
1627
  const program = new Command();
342
- program.name("hardfin").description("Call the Hardfin API from a terminal or an agent").version(version, "-v, --version").showHelpAfterError().enablePositionalOptions();
1628
+ program.name("hardfin").description("Call the Hardfin API from a terminal or an agent").version(version, "-v, --version").option("--api-url <url>", "The API to call, which also moves the authentication endpoints").showHelpAfterError().enablePositionalOptions();
343
1629
  for (const command of commands) program.addCommand(toProgram(command));
344
1630
  await program.parseAsync(process.argv);
345
1631
  /** toProgram wires one registry command into the parser. */
@@ -358,6 +1644,8 @@ function toProgram(command) {
358
1644
  program.addOption(option);
359
1645
  }
360
1646
  for (const example of command.examples) program.addHelpText("after", `\n${example.description}:\n $ ${example.command}`);
1647
+ for (const subcommand of command.subcommands ?? []) program.addCommand(toProgram(subcommand));
1648
+ if (!command.run) return program;
361
1649
  program.action(async (...parsed) => {
362
1650
  const flags = parsed[parsed.length - 2] ?? {};
363
1651
  const args = parsed.slice(0, parsed.length - 2).flatMap(toArgumentList);
@@ -367,16 +1655,23 @@ function toProgram(command) {
367
1655
  }
368
1656
  async function toExitCode(command, args, flags) {
369
1657
  const isJSON = isJSONOutput(flags);
1658
+ const rejected = toRejectedFlag(command, flags);
1659
+ if (rejected) {
1660
+ writeFailure(rejected, isJSON);
1661
+ return ExitCode.USAGE;
1662
+ }
370
1663
  try {
371
- return await command.run({
1664
+ const resolved = toSettings({ apiUrl: program.opts()["apiUrl"] });
1665
+ return await command.run?.({
372
1666
  args,
373
1667
  flags,
374
1668
  isJSON,
375
- commands
376
- });
1669
+ commands,
1670
+ resolved
1671
+ }) ?? ExitCode.OK;
377
1672
  } catch (error) {
378
1673
  writeFailure(error instanceof Error ? error.message : String(error), isJSON);
379
- return ExitCode.ERROR;
1674
+ return error instanceof ConfigFailure ? ExitCode.USAGE : ExitCode.ERROR;
380
1675
  }
381
1676
  }
382
1677
  function toArgumentList(value) {