@hardfin/cli 0.0.2-dev.16 → 0.0.2-dev.17

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 +7 -0
  2. package/dist/cli.js +103 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -71,6 +71,13 @@ A value is sent as the type the document names. `--useful-life 36` sends the num
71
71
  decimal such as `--salvage-value 1500.00` stays a string, which is how the API takes money
72
72
  without losing precision.
73
73
 
74
+ A download is written where you name it, and a terminal is never filled with a file's bytes.
75
+
76
+ ```sh
77
+ hardfin file get file_7hq2mx9pkr4stz8w --output photo.jpg
78
+ hardfin file get file_7hq2mx9pkr4stz8w --output - | wc -c
79
+ ```
80
+
74
81
  Clear a field with `--unset`, naming the flag:
75
82
 
76
83
  ```sh
package/dist/cli.js CHANGED
@@ -258,7 +258,8 @@ function toSummary(command) {
258
258
  valueName: flag.valueName ?? null,
259
259
  repeatable: flag.repeatable ?? false
260
260
  })),
261
- examples: command.examples
261
+ examples: command.examples,
262
+ subcommands: command.subcommands?.filter((entry) => !entry.hidden).map(toSummary)
262
263
  };
263
264
  }
264
265
  //#endregion
@@ -719,11 +720,11 @@ var RequestFailure = class extends Error {
719
720
  /** request calls one /v2 endpoint and returns the envelope it answered with. */
720
721
  async function request(options) {
721
722
  const url = new URL(`${options.apiUrl}${toLeadingSlash(options.path)}`);
722
- if (options.query) url.search = options.query.toString();
723
+ for (const [name, value] of options.query ?? []) url.searchParams.append(name, value);
723
724
  const headers = {
724
725
  [options.credential.header]: options.credential.value,
725
726
  "X-API-Version": API_VERSION,
726
- Accept: "application/json"
727
+ Accept: options.downloads ? "*/*" : "application/json"
727
728
  };
728
729
  if (options.body !== void 0 && options.form === void 0) headers["Content-Type"] = "application/json";
729
730
  const response = await fetch(url, {
@@ -731,6 +732,11 @@ async function request(options) {
731
732
  headers,
732
733
  body: options.form ?? (options.body === void 0 ? void 0 : JSON.stringify(options.body))
733
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
+ } };
734
740
  const envelope = toEnvelope(await response.text());
735
741
  if (!response.ok && envelope === void 0) throw new RequestFailure(response.status, [{
736
742
  error: toStatusMessage(response.status),
@@ -745,6 +751,11 @@ async function request(options) {
745
751
  }
746
752
  return envelope ?? { data: null };
747
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
+ }
748
759
  function toLeadingSlash(path) {
749
760
  return path.startsWith("/") ? path : `/${path}`;
750
761
  }
@@ -1674,7 +1685,21 @@ function toTree(commands) {
1674
1685
  subcommands: command.subcommands ? toTree(command.subcommands) : void 0
1675
1686
  }));
1676
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
+ ]);
1677
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
+ };
1678
1703
  const args = (params?.["arguments"])?.args;
1679
1704
  if (!Array.isArray(args) || args.some((entry) => typeof entry !== "string")) return {
1680
1705
  content: [{
@@ -1683,7 +1708,15 @@ async function toToolResult(params, run) {
1683
1708
  }],
1684
1709
  isError: true
1685
1710
  };
1686
- const outcome = await run(args);
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);
1687
1720
  return {
1688
1721
  content: [{
1689
1722
  type: "text",
@@ -1692,6 +1725,14 @@ async function toToolResult(params, run) {
1692
1725
  isError: outcome.code !== 0
1693
1726
  };
1694
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) {
1730
+ try {
1731
+ return JSON.parse(line).id ?? null;
1732
+ } catch {
1733
+ return null;
1734
+ }
1735
+ }
1695
1736
  /** toCliRunner runs the CLI itself, so a tool call parses exactly as a terminal would. */
1696
1737
  function toCliRunner() {
1697
1738
  return (args) => new Promise((resolve) => {
@@ -1727,9 +1768,9 @@ async function serve(commands, run = toCliRunner()) {
1727
1768
  } catch (error) {
1728
1769
  response = {
1729
1770
  jsonrpc: "2.0",
1730
- id: null,
1771
+ id: toRequestId(line),
1731
1772
  error: {
1732
- code: -32700,
1773
+ code: -32603,
1733
1774
  message: error instanceof Error ? error.message : String(error)
1734
1775
  }
1735
1776
  };
@@ -1784,6 +1825,12 @@ const FILE_FLAG = {
1784
1825
  valueName: "path",
1785
1826
  schema: z.string()
1786
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
+ };
1787
1834
  const JSON_FLAG = {
1788
1835
  name: "json",
1789
1836
  description: "Print machine-readable output, which is the default when stdout is not a terminal",
@@ -1796,6 +1843,7 @@ function defineOperation(operation) {
1796
1843
  ...operation.bodyFlags,
1797
1844
  ...operation.bodyFlags.some((flag) => flag.nullable) ? [UNSET_FLAG] : [],
1798
1845
  ...operation.upload ? [...operation.upload.fields, FILE_FLAG] : [],
1846
+ ...operation.downloads ? [OUTPUT_FLAG] : [],
1799
1847
  JSON_FLAG
1800
1848
  ];
1801
1849
  return {
@@ -1841,15 +1889,18 @@ async function runOperation(operation, input) {
1841
1889
  form = built;
1842
1890
  }
1843
1891
  try {
1844
- writeData((await request({
1892
+ const envelope = await request({
1845
1893
  apiUrl: input.resolved.settings.apiUrl,
1846
1894
  credential: await toRequestCredential(input.resolved.settings),
1847
1895
  method: operation.method,
1848
1896
  path,
1849
1897
  query: toQuery(operation, input.flags),
1850
1898
  body,
1851
- form
1852
- })).data);
1899
+ form,
1900
+ downloads: operation.downloads
1901
+ });
1902
+ if (operation.downloads) return toWritten(envelope.data, input);
1903
+ writeData(envelope.data);
1853
1904
  return ExitCode.OK;
1854
1905
  } catch (error) {
1855
1906
  if (error instanceof NoCredential) {
@@ -1864,6 +1915,29 @@ async function runOperation(operation, input) {
1864
1915
  return ExitCode.ERROR;
1865
1916
  }
1866
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
+ }
1867
1941
  /** toPath fills the path template from the positional arguments, in order. */
1868
1942
  function toPath(operation, args) {
1869
1943
  if (args.length !== operation.pathParameters.length) return;
@@ -2593,6 +2667,7 @@ const surfaceCommands = [
2593
2667
  name: "is-in-service-date-managed-automatically",
2594
2668
  jsonPath: ["isInServiceDateManagedAutomatically"],
2595
2669
  description: "Whether Hardfin sets the in-service date itself, which sending an in-service date turns off",
2670
+ negatable: true,
2596
2671
  nullable: true,
2597
2672
  schema: z.boolean()
2598
2673
  },
@@ -2659,6 +2734,7 @@ const surfaceCommands = [
2659
2734
  name: "automatic",
2660
2735
  jsonPath: ["automatic"],
2661
2736
  description: "Whether Hardfin sets the asset's in-service date itself",
2737
+ negatable: true,
2662
2738
  required: true,
2663
2739
  schema: z.boolean()
2664
2740
  }]
@@ -3361,12 +3437,14 @@ const surfaceCommands = [
3361
3437
  name: "is-customer",
3362
3438
  jsonPath: ["isCustomer"],
3363
3439
  description: "Whether the company is a customer",
3440
+ negatable: true,
3364
3441
  schema: z.boolean()
3365
3442
  },
3366
3443
  {
3367
3444
  name: "is-supplier",
3368
3445
  jsonPath: ["isSupplier"],
3369
3446
  description: "Whether the company is a supplier",
3447
+ negatable: true,
3370
3448
  schema: z.boolean()
3371
3449
  },
3372
3450
  {
@@ -3466,6 +3544,7 @@ const surfaceCommands = [
3466
3544
  name: "is-archived",
3467
3545
  jsonPath: ["isArchived"],
3468
3546
  description: "Whether the customer is archived",
3547
+ negatable: true,
3469
3548
  nullable: true,
3470
3549
  schema: z.boolean()
3471
3550
  },
@@ -3473,6 +3552,7 @@ const surfaceCommands = [
3473
3552
  name: "is-customer",
3474
3553
  jsonPath: ["isCustomer"],
3475
3554
  description: "Whether the company is a customer",
3555
+ negatable: true,
3476
3556
  nullable: true,
3477
3557
  schema: z.boolean()
3478
3558
  },
@@ -3480,6 +3560,7 @@ const surfaceCommands = [
3480
3560
  name: "is-supplier",
3481
3561
  jsonPath: ["isSupplier"],
3482
3562
  description: "Whether the company is a supplier",
3563
+ negatable: true,
3483
3564
  nullable: true,
3484
3565
  schema: z.boolean()
3485
3566
  },
@@ -3554,7 +3635,8 @@ const surfaceCommands = [
3554
3635
  description: "True when the file downloads as an attachment rather than opening inline",
3555
3636
  schema: z.boolean()
3556
3637
  }],
3557
- bodyFlags: []
3638
+ bodyFlags: [],
3639
+ downloads: true
3558
3640
  })]
3559
3641
  },
3560
3642
  {
@@ -3657,6 +3739,7 @@ const surfaceCommands = [
3657
3739
  name: "accepts-bulk-serials",
3658
3740
  jsonPath: ["acceptsBulkSerials"],
3659
3741
  description: "Whether a BULK item records serial numbers on its units, which SERVICE and DEVICE items ignore",
3742
+ negatable: true,
3660
3743
  schema: z.boolean()
3661
3744
  },
3662
3745
  {
@@ -3793,6 +3876,7 @@ const surfaceCommands = [
3793
3876
  name: "accepts-bulk-serials",
3794
3877
  jsonPath: ["acceptsBulkSerials"],
3795
3878
  description: "Whether a BULK item records serial numbers on its units, read only beside type",
3879
+ negatable: true,
3796
3880
  nullable: true,
3797
3881
  schema: z.boolean()
3798
3882
  },
@@ -3821,6 +3905,7 @@ const surfaceCommands = [
3821
3905
  name: "is-archived",
3822
3906
  jsonPath: ["isArchived"],
3823
3907
  description: "Whether the item is archived, which cannot be null",
3908
+ negatable: true,
3824
3909
  nullable: true,
3825
3910
  schema: z.boolean()
3826
3911
  },
@@ -4290,18 +4375,21 @@ const surfaceCommands = [
4290
4375
  name: "is-inventory",
4291
4376
  jsonPath: ["isInventory"],
4292
4377
  description: "Whether assets at the location count as inventory for reporting",
4378
+ negatable: true,
4293
4379
  schema: z.boolean()
4294
4380
  },
4295
4381
  {
4296
4382
  name: "is-inventory-override",
4297
4383
  jsonPath: ["isInventoryOverride"],
4298
4384
  description: "Whether a zone sets its own isInventory rather than inheriting its parent site's, which a site refuses",
4385
+ negatable: true,
4299
4386
  schema: z.boolean()
4300
4387
  },
4301
4388
  {
4302
4389
  name: "is-transient",
4303
4390
  jsonPath: ["isTransient"],
4304
4391
  description: "Whether assets make only occasional or temporary stops at the location, which hides it from location lists by default",
4392
+ negatable: true,
4305
4393
  schema: z.boolean()
4306
4394
  },
4307
4395
  {
@@ -4443,6 +4531,7 @@ const surfaceCommands = [
4443
4531
  name: "is-archived",
4444
4532
  jsonPath: ["isArchived"],
4445
4533
  description: "Whether the location is archived, and archiving a site archives its zones",
4534
+ negatable: true,
4446
4535
  nullable: true,
4447
4536
  schema: z.boolean()
4448
4537
  },
@@ -4450,6 +4539,7 @@ const surfaceCommands = [
4450
4539
  name: "is-inventory",
4451
4540
  jsonPath: ["isInventory"],
4452
4541
  description: "Whether assets at the location count as inventory for reporting",
4542
+ negatable: true,
4453
4543
  nullable: true,
4454
4544
  schema: z.boolean()
4455
4545
  },
@@ -4457,6 +4547,7 @@ const surfaceCommands = [
4457
4547
  name: "is-inventory-override",
4458
4548
  jsonPath: ["isInventoryOverride"],
4459
4549
  description: "Whether a zone sets its own isInventory rather than inheriting its parent site's, which a site refuses",
4550
+ negatable: true,
4460
4551
  nullable: true,
4461
4552
  schema: z.boolean()
4462
4553
  },
@@ -4464,6 +4555,7 @@ const surfaceCommands = [
4464
4555
  name: "is-transient",
4465
4556
  jsonPath: ["isTransient"],
4466
4557
  description: "Whether assets make only occasional or temporary stops at the location, which hides it from location lists by default",
4558
+ negatable: true,
4467
4559
  nullable: true,
4468
4560
  schema: z.boolean()
4469
4561
  },
@@ -4887,6 +4979,7 @@ function toProgram(command) {
4887
4979
  const short = flag.short ? `-${flag.short}, ` : "";
4888
4980
  const value = flag.valueName ? ` <${flag.valueName}>` : "";
4889
4981
  const option = new Option(`${short}--${flag.name}${value}`, flag.description);
4982
+ if (flag.negatable) program.addOption(new Option(`--no-${flag.name}`, `${flag.description}, turned off`));
4890
4983
  if (flag.repeatable) option.argParser(collect);
4891
4984
  if (flag.defaultValue !== void 0) option.default(flag.defaultValue);
4892
4985
  program.addOption(option);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardfin/cli",
3
- "version": "0.0.2-dev.16",
3
+ "version": "0.0.2-dev.17",
4
4
  "description": "Command line interface for the Hardfin API",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hardfin, Inc.",