@hardfin/cli 0.1.0-dev.32 → 0.1.0-dev.34

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.
package/README.md CHANGED
@@ -208,6 +208,21 @@ On WSL the browser is opened through Windows, by `wslview`, then PowerShell, the
208
208
  The port changes every time, so Hardfin matches a loopback redirect by everything except its
209
209
  port, which is what RFC 8252 asks of an authorization server.
210
210
 
211
+ ### When an agent signs you in
212
+
213
+ An agent that runs `hardfin login` reads its stderr, and you only see what the agent passes
214
+ on. So when output is JSON, which it is whenever stdout is not a terminal, the command writes
215
+ one line of JSON on stderr before it starts waiting.
216
+
217
+ | `event` | Also carries | The agent should |
218
+ | ------------- | -------------------------------------------------------------------- | --------------------------------------------------- |
219
+ | `sign_in_url` | `url`, `browserOpened`, `waitSeconds` | Show you `url`, saying whether a browser tab opened |
220
+ | `device_code` | `userCode`, `verificationUri`, `url`, `browserOpened`, `waitSeconds` | Show you the code and where to enter it |
221
+
222
+ The command then waits up to `waitSeconds` for you to approve, so the agent has to let it run
223
+ that long. An agent with no way to open your browser, such as one in a cloud sandbox, signs
224
+ in with `--device`.
225
+
211
226
  ### Signing in from a machine with no browser
212
227
 
213
228
  `hardfin login --device` prints a short code and a page to enter it on, and waits. Approve
@@ -305,22 +320,33 @@ other client can run Hardfin commands.
305
320
  claude mcp add hardfin -- hardfin mcp
306
321
  ```
307
322
 
308
- ### One tool, and resources for the rest
323
+ ### Two tools, and resources for the rest
309
324
 
310
325
  Every tool a server lists sits in the agent's context for the entire session, so this server
311
- offers one.
326
+ offers two. They differ only in what they may run, which lets a client run a read without
327
+ asking and ask before anything changes.
312
328
 
313
- | Offered as | Holds | Costs context |
314
- | -------------------- | ---------------------------------------------------------------- | --------------------------------- |
315
- | The `hardfin` tool | A list of arguments, such as `["asset", "list", "--limit", "5"]` | Always, and it is about 400 bytes |
316
- | `hardfin://guide` | Every command, its flags, and the exit codes | Only when the agent reads it |
317
- | `hardfin://commands` | The command tree as JSON | Only when the agent reads it |
329
+ | Offered as | Holds | Costs context |
330
+ | -------------------- | ---------------------------------------------------------------------- | ---------------------------- |
331
+ | `hardfin_read` | A command that only reads, such as `["asset", "list", "--limit", "5"]` | Always, about 800 bytes |
332
+ | `hardfin_write` | A command that creates, changes or deletes, with an optional JSON body | Always, about 800 bytes |
333
+ | `hardfin://guide` | Every command, its flags, the exit codes and the error codes | Only when the agent reads it |
334
+ | `hardfin://commands` | The command tree as JSON | Only when the agent reads it |
318
335
 
319
- Fifty tools would describe the same surface and crowd out the work. An agent that needs the
320
- list reads a resource, or runs `["--help"]`.
336
+ A command is a read when its endpoint's method is GET, or for `hardfin api` when no other
337
+ `--method` is given. Anything else is a write, and `hardfin_read` refuses it by naming
338
+ `hardfin_write`. Both tools carry a title, the read-only or destructive hint, and a link to
339
+ the API reference, which Claude's connector directory requires.
321
340
 
322
341
  A tool call runs the CLI itself, so an agent and a person get identical parsing, output and
323
- exit codes.
342
+ exit codes. Each call has these limits:
343
+
344
+ | Limit | Value | Past it |
345
+ | ---------------- | --------------------------------------- | ------------------------------------------ |
346
+ | Time | 120 seconds | The command is stopped, and the call fails |
347
+ | Commands running | 4 at once | The next call waits for one to finish |
348
+ | One argument | 128 KiB | Refused, pointing at the `input` argument |
349
+ | Standard input | The call's `input` argument, or nothing | Always closed, so `--input -` never waits |
324
350
 
325
351
  ### What it signs in as
326
352
 
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="62437fed-ab84-53c5-9984-9bbadc0f2e15")}catch(e){}}();
3
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="f2529e6e-d7d4-5fe2-b460-7e86ca865c25")}catch(e){}}();
4
4
  import module from "node:module";
5
5
  //#region src/cli.ts
6
6
  module.enableCompileCache?.();
@@ -14,7 +14,7 @@ process.on("unhandledRejection", (error) => void toCrash(error));
14
14
  try {
15
15
  const answer = await toQuickAnswer(process.argv.slice(2));
16
16
  if (answer === void 0) {
17
- const { toCli, toParseExitCode, wasReportAttempted } = await import("./program-B02z-kNH.js");
17
+ const { toCli, toParseExitCode, wasReportAttempted } = await import("./program-DTsxnjRj.js");
18
18
  try {
19
19
  await toCli().parseAsync(process.argv);
20
20
  } catch (error) {
@@ -33,7 +33,7 @@ async function toCrash(error) {
33
33
  isCrashing = true;
34
34
  process.stderr.write(`error: ${error instanceof Error ? error.message : String(error)}\n`);
35
35
  try {
36
- const { toReported } = await import("./program-B02z-kNH.js");
36
+ const { toReported } = await import("./program-DTsxnjRj.js");
37
37
  await toReported(error, false, void 0, "fatal");
38
38
  } finally {
39
39
  process.exit(1);
@@ -51,4 +51,4 @@ async function toQuickAnswer(args) {
51
51
  export {};
52
52
 
53
53
  //# sourceMappingURL=cli.js.map
54
- //# debugId=62437fed-ab84-53c5-9984-9bbadc0f2e15
54
+ //# debugId=f2529e6e-d7d4-5fe2-b460-7e86ca865c25
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="b1928379-e7e3-5a7e-9ade-d45695e05814")}catch(e){}}();
3
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="c7f8ce05-d638-5d0f-a088-655ddade6f45")}catch(e){}}();
4
4
  import { a as defineCommand, i as GLOBAL_FLAGS, r as ExitCode, t as toInvocation } from "./scrub-BN3gyBpR.js";
5
5
  import { n as toCompletion, t as toAnswer$1 } from "./completion-tree-rR_Rmeij.js";
6
6
  import { createRequire } from "node:module";
@@ -987,6 +987,7 @@ function toGuide(commands, version) {
987
987
  "## Before calling",
988
988
  "",
989
989
  "- Authenticate with `--api-key`, by setting `HARDFIN_API_KEY`, or by running `hardfin login`",
990
+ "- `hardfin login` writes one JSON line on stderr before it waits. Show the person its `url`, or its `userCode` and `verificationUri`, and let the command run for `waitSeconds`",
990
991
  "- Every command prints JSON on stdout and diagnostics on stderr",
991
992
  "- Output is JSON whenever stdout is not a terminal, so no flag is needed when calling from code",
992
993
  "",
@@ -2154,7 +2155,13 @@ async function runLogin(input) {
2154
2155
  challenge: pkce.challenge
2155
2156
  });
2156
2157
  const opened = input.flags["noBrowser"] !== true && !process.env["HARDFIN_NO_BROWSER"] && openBrowser(url);
2157
- process.stderr.write(opened ? `Opening your browser to sign in. If it did not open:\n\n${url}\n\n` : `Open this URL to sign in:\n\n${url}\n\n`);
2158
+ if (input.isJSON) writeEvent({
2159
+ event: "sign_in_url",
2160
+ url,
2161
+ browserOpened: opened,
2162
+ waitSeconds: WAIT_MS / 1e3
2163
+ });
2164
+ else process.stderr.write(opened ? `Opening your browser to sign in. If it did not open:\n\n${url}\n\n` : `Open this URL to sign in:\n\n${url}\n\n`);
2158
2165
  const prompt = toPrompt(url);
2159
2166
  if (process.stdin.isTTY) process.stderr.write("Press c to copy the URL, or paste the code or redirect URL here: ");
2160
2167
  let callback;
@@ -2164,7 +2171,7 @@ async function runLogin(input) {
2164
2171
  listener.close();
2165
2172
  prompt.close();
2166
2173
  }
2167
- process.stderr.write(callback.error ? "\n" : "\nApproved, finishing the sign in\n");
2174
+ if (!input.isJSON) process.stderr.write(callback.error ? "\n" : "\nApproved, finishing the sign in\n");
2168
2175
  if (callback.error) {
2169
2176
  writeFailure("sign_in_failed", `sign in was refused: ${callback.error}${callback.errorDescription ? `, ${callback.errorDescription}` : ""}`, input.isJSON);
2170
2177
  return ExitCode.ERROR;
@@ -2200,7 +2207,15 @@ async function runDeviceLogin(input, metadata, scope) {
2200
2207
  const device = await toDeviceAuthorization(metadata.device_authorization_endpoint, settings.clientId, scope);
2201
2208
  const url = device.verificationUriComplete ?? device.verificationUri;
2202
2209
  const opened = input.flags["noBrowser"] !== true && !process.env["HARDFIN_NO_BROWSER"] && openBrowser(url);
2203
- process.stderr.write([
2210
+ if (input.isJSON) writeEvent({
2211
+ event: "device_code",
2212
+ userCode: device.userCode,
2213
+ verificationUri: device.verificationUri,
2214
+ url,
2215
+ browserOpened: opened,
2216
+ waitSeconds: Math.max(0, Math.round((device.expiresAt - Date.now()) / 1e3))
2217
+ });
2218
+ else process.stderr.write([
2204
2219
  "",
2205
2220
  ` Code ${device.userCode}`,
2206
2221
  ` At ${device.verificationUri}`,
@@ -2252,6 +2267,10 @@ async function toSignedIn(input, metadata, tokens) {
2252
2267
  ].join("\n"));
2253
2268
  return ExitCode.OK;
2254
2269
  }
2270
+ /** writeEvent tells a program what a sign in is waiting on, as one line of JSON on stderr. */
2271
+ function writeEvent(event) {
2272
+ process.stderr.write(`${JSON.stringify(event)}\n`);
2273
+ }
2255
2274
  /** toAuthorizationUrl builds the URL the person approves this CLI at. */
2256
2275
  function toAuthorizationUrl(endpoint, request) {
2257
2276
  const url = new URL(endpoint);
@@ -2778,23 +2797,52 @@ function wasReportAttempted() {
2778
2797
  const PROTOCOL_VERSION = "2025-06-18";
2779
2798
  const GUIDE_URI = "hardfin://guide";
2780
2799
  const COMMANDS_URI = "hardfin://commands";
2781
- /**
2782
- * One tool, because every tool a server lists sits in the agent's context for the whole
2783
- * session. The commands themselves are resources, which cost nothing until one is read.
2784
- */
2785
- const TOOL = {
2786
- name: "hardfin",
2787
- description: "Run a Hardfin CLI command against the Hardfin API. Pass the arguments as a list, such as [\"asset\", \"list\", \"--limit\", \"5\"]. Run [\"--help\"] for the commands, or read the hardfin://guide resource.",
2788
- inputSchema: {
2789
- type: "object",
2790
- properties: { args: {
2800
+ /** The public reference for the API every command calls. */
2801
+ const API_REFERENCE = "https://docs.hardfin.com";
2802
+ const ARGUMENTS_SCHEMA = {
2803
+ type: "object",
2804
+ properties: {
2805
+ args: {
2791
2806
  type: "array",
2792
2807
  items: { type: "string" },
2793
2808
  description: "The arguments to hardfin, without the program name"
2794
- } },
2795
- required: ["args"]
2809
+ },
2810
+ input: {
2811
+ type: "string",
2812
+ description: "JSON sent to the command on standard input, which it reads when given --input -"
2813
+ }
2814
+ },
2815
+ required: ["args"]
2816
+ };
2817
+ /**
2818
+ * Two tools, so a client can let a read run without asking and ask before anything changes.
2819
+ * The commands themselves are resources, which cost nothing until one is read.
2820
+ */
2821
+ const READ_TOOL = {
2822
+ name: "hardfin_read",
2823
+ title: "Read Hardfin data",
2824
+ description: `Runs a Hardfin CLI command that only reads, against the Hardfin API (${API_REFERENCE}). Takes the arguments as a list, such as ["asset", "list", "--limit", "5"]. ["--help"] lists the commands, and the hardfin://guide resource describes each one.`,
2825
+ inputSchema: ARGUMENTS_SCHEMA,
2826
+ annotations: {
2827
+ title: "Read Hardfin data",
2828
+ readOnlyHint: true,
2829
+ destructiveHint: false,
2830
+ openWorldHint: true
2831
+ }
2832
+ };
2833
+ const WRITE_TOOL = {
2834
+ name: "hardfin_write",
2835
+ title: "Change Hardfin data",
2836
+ description: `Runs a Hardfin CLI command that creates, changes or deletes records through the Hardfin API (${API_REFERENCE}). Takes the arguments as a list, such as ["customer", "create", "--name", "Acme"]. A JSON body goes in the input argument, with --input - in the list.`,
2837
+ inputSchema: ARGUMENTS_SCHEMA,
2838
+ annotations: {
2839
+ title: "Change Hardfin data",
2840
+ readOnlyHint: false,
2841
+ destructiveHint: true,
2842
+ openWorldHint: true
2796
2843
  }
2797
2844
  };
2845
+ const INSTRUCTIONS = "hardfin_read looks records up and hardfin_write changes them. Both run Hardfin CLI commands, so the arguments are what a person would type after hardfin. The hardfin://guide resource lists every command, flag and error code.";
2798
2846
  /** ProtocolFailure is a line this server answers with a JSON-RPC error rather than a result. */
2799
2847
  var ProtocolFailure = class extends Error {
2800
2848
  code;
@@ -2822,10 +2870,12 @@ async function toResponse(request, commands, run) {
2822
2870
  },
2823
2871
  serverInfo: {
2824
2872
  name: "hardfin",
2873
+ title: "Hardfin",
2825
2874
  version: version$1
2826
- }
2875
+ },
2876
+ instructions: INSTRUCTIONS
2827
2877
  });
2828
- case "tools/list": return answer({ tools: [TOOL] });
2878
+ case "tools/list": return answer({ tools: [READ_TOOL, WRITE_TOOL] });
2829
2879
  case "resources/list": return answer({ resources: [{
2830
2880
  uri: GUIDE_URI,
2831
2881
  name: "Hardfin CLI guide",
@@ -2838,7 +2888,8 @@ async function toResponse(request, commands, run) {
2838
2888
  mimeType: "application/json"
2839
2889
  }] });
2840
2890
  case "resources/read": {
2841
- const uri = toText(request.params?.["uri"] ?? "");
2891
+ const uri = request.params?.["uri"];
2892
+ if (typeof uri !== "string" || uri === "") throw new ProtocolFailure(-32602, "resources/read needs a uri, such as hardfin://guide", request.id ?? null);
2842
2893
  const resource = toResource(uri, commands);
2843
2894
  if (!resource) return {
2844
2895
  jsonrpc: "2.0",
@@ -2850,7 +2901,7 @@ async function toResponse(request, commands, run) {
2850
2901
  };
2851
2902
  return answer(resource);
2852
2903
  }
2853
- case "tools/call": return answer(await toToolResult(request.params, run));
2904
+ case "tools/call": return answer(await toToolResult(request, commands, run));
2854
2905
  case "ping": return answer({});
2855
2906
  default:
2856
2907
  if (request.id === void 0 || request.id === null) return;
@@ -2924,80 +2975,170 @@ function toCommandName(args) {
2924
2975
  function toIsAskingForHelp(args) {
2925
2976
  return args.includes("--help") || args.includes("-h") || args[0] === "help";
2926
2977
  }
2927
- async function toToolResult(params, run) {
2928
- if (params?.["name"] !== TOOL.name) return {
2929
- content: [{
2930
- type: "text",
2931
- text: `a call names its tool, and this server offers one, ${TOOL.name}`
2932
- }],
2933
- isError: true
2934
- };
2935
- const args = (params?.["arguments"])?.args;
2936
- if (!Array.isArray(args) || args.some((entry) => typeof entry !== "string")) return {
2937
- content: [{
2938
- type: "text",
2939
- text: "args must be a list of strings, such as [\"asset\", \"list\"]"
2940
- }],
2941
- isError: true
2942
- };
2978
+ async function toToolResult(request, commands, run) {
2979
+ const params = request.params;
2980
+ const name = params?.["name"];
2981
+ if (name !== READ_TOOL.name && name !== WRITE_TOOL.name) throw new ProtocolFailure(-32602, `a call names its tool, and this server offers ${READ_TOOL.name} and ${WRITE_TOOL.name}`, request.id ?? null);
2982
+ const called = params?.["arguments"];
2983
+ const args = called?.args;
2984
+ if (!Array.isArray(args) || args.some((entry) => typeof entry !== "string")) return toError("args must be a list of strings, such as [\"asset\", \"list\"]");
2985
+ if (called?.input !== void 0 && typeof called.input !== "string") return toError("input must be a string holding JSON, such as {\"name\": \"Acme\"}");
2943
2986
  const refused = toRefusedArgument(args);
2944
- if (refused) return {
2945
- content: [{
2946
- type: "text",
2947
- text: refused
2948
- }],
2949
- isError: true
2950
- };
2987
+ if (refused) return toError(refused);
2951
2988
  const asked = args;
2952
2989
  const command = toCommandName(asked) ?? "";
2953
- if (REFUSED_COMMANDS.has(command) && !toIsAskingForHelp(asked)) return {
2990
+ if (REFUSED_COMMANDS.has(command) && !toIsAskingForHelp(asked)) return toError(`${command} is run by a person at a terminal, not through this server. Run hardfin ${command} yourself, then call this tool again`);
2991
+ if (name === READ_TOOL.name && toAccess(asked, commands) === "write") return toError(`this command changes data, so it runs through ${WRITE_TOOL.name}`);
2992
+ const outcome = await run(asked, called?.input);
2993
+ return {
2954
2994
  content: [{
2955
2995
  type: "text",
2956
- text: `${command} is run by a person at a terminal, not through this server. Run hardfin ${command} yourself, then call this tool again`
2996
+ text: [outcome.stdout, outcome.stderr].filter((part) => part.trim() !== "").join("\n") || `hardfin exited ${outcome.code}`
2957
2997
  }],
2958
- isError: true
2998
+ isError: outcome.code !== 0
2959
2999
  };
2960
- const outcome = await run(asked);
3000
+ }
3001
+ function toError(text) {
2961
3002
  return {
2962
3003
  content: [{
2963
3004
  type: "text",
2964
- text: [outcome.stdout, outcome.stderr].filter((part) => part.trim() !== "").join("\n") || `hardfin exited ${outcome.code}`
3005
+ text
2965
3006
  }],
2966
- isError: outcome.code !== 0
3007
+ isError: true
2967
3008
  };
2968
3009
  }
2969
- /** The most a command line can carry, below what the operating system refuses outright. */
2970
- const ARGUMENT_LIMIT = 1e5;
3010
+ /** Commands with no endpoint that only show something, which a read may run. */
3011
+ const LOCAL_READS = /* @__PURE__ */ new Set([
3012
+ "status",
3013
+ "config",
3014
+ "agent-guide",
3015
+ "help"
3016
+ ]);
3017
+ /**
3018
+ * toAccess decides whether a call reads or changes data. A generated command answers by its
3019
+ * endpoint's method, hardfin api by the method it was given, and anything it cannot place is
3020
+ * treated as a change, so it is asked about.
3021
+ */
3022
+ function toAccess(args, commands) {
3023
+ if (toIsAskingForHelp(args)) return "read";
3024
+ let level = commands;
3025
+ let found;
3026
+ for (let index = 0; index < args.length; index += 1) {
3027
+ const word = args[index] ?? "";
3028
+ if (GLOBAL_OPTIONS_WITH_VALUES.has(word)) {
3029
+ index += 1;
3030
+ continue;
3031
+ }
3032
+ if (word.startsWith("-")) continue;
3033
+ const next = level.find((entry) => entry.name === word);
3034
+ if (!next) break;
3035
+ found = next;
3036
+ level = next.subcommands ?? [];
3037
+ if (level.length === 0) break;
3038
+ }
3039
+ if (!found) return "read";
3040
+ if (found.endpoint) return found.endpoint.method === "GET" ? "read" : "write";
3041
+ if (found.name === "api") return toApiMethod(args) === "GET" ? "read" : "write";
3042
+ if (LOCAL_READS.has(found.name) || (found.subcommands?.length ?? 0) > 0) return "read";
3043
+ return "write";
3044
+ }
3045
+ /** toApiMethod reads the method hardfin api was given, which is GET when none was. */
3046
+ function toApiMethod(args) {
3047
+ for (let index = 0; index < args.length; index += 1) {
3048
+ const word = args[index] ?? "";
3049
+ if (word === "-X" || word === "--method") return (args[index + 1] ?? "GET").toUpperCase();
3050
+ if (word.startsWith("--method=")) return word.slice(9).toUpperCase();
3051
+ if (word.startsWith("-X") && word.length > 2) return word.slice(2).toUpperCase();
3052
+ }
3053
+ return "GET";
3054
+ }
3055
+ /** The most one argument can hold, which Linux refuses past 128 KiB. */
3056
+ const ARGUMENT_BYTES = 131071;
3057
+ /** The most a whole command line can carry, below what the operating system refuses outright. */
3058
+ const COMMAND_LINE_BYTES = 1e6;
2971
3059
  /** toRefusedArgument names an argument a command line cannot carry, rather than failing later. */
2972
3060
  function toRefusedArgument(args) {
2973
3061
  const withNul = args.findIndex((argument) => argument.includes("\0"));
2974
3062
  if (withNul >= 0) return `args[${withNul}] holds a NUL byte, which a command line cannot carry`;
2975
- const total = args.reduce((carried, argument) => carried + argument.length, 0);
2976
- if (total > ARGUMENT_LIMIT) return `these arguments are ${total} characters, and a command line carries at most ${ARGUMENT_LIMIT}`;
2977
- }
3063
+ const oversized = args.findIndex((argument) => Buffer.byteLength(argument) > ARGUMENT_BYTES);
3064
+ if (oversized >= 0) return `args[${oversized}] is ${Buffer.byteLength(args[oversized] ?? "")} bytes, and one argument carries at most ${ARGUMENT_BYTES}. Send a large body in the input argument instead`;
3065
+ const total = args.reduce((carried, argument) => carried + Buffer.byteLength(argument), 0);
3066
+ if (total > COMMAND_LINE_BYTES) return `these arguments are ${total} bytes, and a command line carries at most ${COMMAND_LINE_BYTES}`;
3067
+ }
3068
+ /** How long one command may run before it is stopped, so a hung call cannot hold its client. */
3069
+ const CALL_TIMEOUT_MS = 12e4;
3070
+ /** How many commands run at once, so a burst of calls cannot start a process each. */
3071
+ const MAX_RUNNING = 4;
2978
3072
  /** toCliRunner runs the CLI itself, so a tool call parses exactly as a terminal would. */
2979
- function toCliRunner() {
2980
- return (args) => new Promise((resolve) => {
2981
- const child = spawn(process.execPath, [process.argv[1] ?? "", ...args], { env: {
2982
- ...process.env,
2983
- HARDFIN_NO_BROWSER: "1",
2984
- HARDFIN_MCP: "1"
2985
- } });
3073
+ function toCliRunner(timeoutMs = CALL_TIMEOUT_MS, entry = process.argv[1] ?? "") {
3074
+ const waiting = [];
3075
+ let running = 0;
3076
+ const acquire = async () => {
3077
+ if (running < MAX_RUNNING) {
3078
+ running += 1;
3079
+ return;
3080
+ }
3081
+ await new Promise((resolve) => waiting.push(resolve));
3082
+ };
3083
+ const release = () => {
3084
+ const next = waiting.shift();
3085
+ if (next) next();
3086
+ else running -= 1;
3087
+ };
3088
+ return async (args, input) => {
3089
+ await acquire();
3090
+ try {
3091
+ return await toRun(entry, args, input, timeoutMs);
3092
+ } finally {
3093
+ release();
3094
+ }
3095
+ };
3096
+ }
3097
+ /** toRun runs one command, feeding it input on stdin and stopping it at the deadline. */
3098
+ function toRun(entry, args, input, timeoutMs) {
3099
+ return new Promise((resolve) => {
3100
+ let child;
3101
+ try {
3102
+ child = spawn(process.execPath, [entry, ...args], { env: {
3103
+ ...process.env,
3104
+ HARDFIN_NO_BROWSER: "1",
3105
+ HARDFIN_MCP: "1"
3106
+ } });
3107
+ } catch (error) {
3108
+ resolve({
3109
+ stdout: "",
3110
+ stderr: `the command could not start: ${error instanceof Error ? error.message : String(error)}`,
3111
+ code: 1
3112
+ });
3113
+ return;
3114
+ }
2986
3115
  let stdout = "";
2987
3116
  let stderr = "";
3117
+ let isTimedOut = false;
3118
+ const timer = setTimeout(() => {
3119
+ isTimedOut = true;
3120
+ child.kill("SIGKILL");
3121
+ }, timeoutMs);
2988
3122
  child.stdout.on("data", (chunk) => {
2989
3123
  stdout += chunk.toString();
2990
3124
  });
2991
3125
  child.stderr.on("data", (chunk) => {
2992
3126
  stderr += chunk.toString();
2993
3127
  });
3128
+ child.on("error", (error) => {
3129
+ stderr += `the command could not start: ${error.message}\n`;
3130
+ });
2994
3131
  child.on("close", (code) => {
3132
+ clearTimeout(timer);
3133
+ if (isTimedOut) stderr += `this command took longer than ${timeoutMs / 1e3} seconds, so it was stopped\n`;
2995
3134
  resolve({
2996
3135
  stdout,
2997
3136
  stderr,
2998
- code: code ?? 1
3137
+ code: isTimedOut ? 1 : code ?? 1
2999
3138
  });
3000
3139
  });
3140
+ child.stdin.on("error", () => void 0);
3141
+ child.stdin.end(input ?? "");
3001
3142
  });
3002
3143
  }
3003
3144
  /** serve answers requests on stdin until the client closes it. */
@@ -6168,5 +6309,5 @@ async function toReported(error, isJSON, resolved, level = "error") {
6168
6309
  //#endregion
6169
6310
  export { toCli, toParseExitCode, toReported, wasReportAttempted };
6170
6311
 
6171
- //# sourceMappingURL=program-B02z-kNH.js.map
6172
- //# debugId=b1928379-e7e3-5a7e-9ade-d45695e05814
6312
+ //# sourceMappingURL=program-DTsxnjRj.js.map
6313
+ //# debugId=c7f8ce05-d638-5d0f-a088-655ddade6f45
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardfin/cli",
3
- "version": "0.1.0-dev.32",
3
+ "version": "0.1.0-dev.34",
4
4
  "description": "Command line interface for the Hardfin API",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hardfin, Inc.",