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

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/dist/cli.js +390 -0
  2. package/package.json +21 -3
  3. package/bin/hardfin.js +0 -27
package/dist/cli.js ADDED
@@ -0,0 +1,390 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { Command, Option } from "commander";
4
+ import { z } from "zod";
5
+ import { readFileSync } from "node:fs";
6
+ //#region src/command/registry.ts
7
+ /** ExitCode is what the process returns, and what an agent branches on. */
8
+ const ExitCode = {
9
+ OK: 0,
10
+ ERROR: 1,
11
+ USAGE: 2,
12
+ NOT_AUTHENTICATED: 4
13
+ };
14
+ /** defineCommand records one command so every surface reads the same declaration. */
15
+ function defineCommand(command) {
16
+ return command;
17
+ }
18
+ //#endregion
19
+ //#region src/config/settings.ts
20
+ /** The API version this build was written against, sent on every request. */
21
+ const API_VERSION = "2026-09-17";
22
+ 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
+ }
27
+ /** toApiKey reads the API key an unattended caller set. */
28
+ function toApiKey() {
29
+ return process.env["HARDFIN_API_KEY"] || void 0;
30
+ }
31
+ //#endregion
32
+ //#region src/output/writer.ts
33
+ /** writeData prints a command's result on stdout. */
34
+ function writeData(value) {
35
+ if (typeof value === "string") {
36
+ process.stdout.write(value.endsWith("\n") ? value : `${value}\n`);
37
+ return;
38
+ }
39
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
40
+ }
41
+ /** writeFailure prints why a command failed on stderr, as text or as JSON. */
42
+ function writeFailure(message, isJSON, errors, requestId) {
43
+ if (!isJSON) {
44
+ process.stderr.write(`error: ${message}\n`);
45
+ for (const entry of errors?.slice(1) ?? []) process.stderr.write(` ${entry.error} (${entry.statusCode})\n`);
46
+ if (requestId) process.stderr.write(` request ${requestId}\n`);
47
+ return;
48
+ }
49
+ const payload = {
50
+ errors: errors ?? [{
51
+ error: message,
52
+ statusCode: 0
53
+ }],
54
+ requestId: requestId ?? null
55
+ };
56
+ process.stderr.write(`${JSON.stringify(payload, null, 2)}\n`);
57
+ }
58
+ /** isJSONOutput decides whether this invocation prints machine-readable output. */
59
+ function isJSONOutput(flags) {
60
+ if (flags["json"] === true) return true;
61
+ return !process.stdout.isTTY;
62
+ }
63
+ /** version is what this build reports, read from the published package manifest. */
64
+ const version = createRequire(import.meta.url)("../package.json").version;
65
+ //#endregion
66
+ //#region src/command/agent-guide.ts
67
+ const agentGuideCommand = defineCommand({
68
+ name: "agent-guide",
69
+ summary: "Print instructions an agent can follow to use this CLI",
70
+ description: "Writes Markdown describing every command, its flags, and its exit codes. Save it as a skill file, an AGENTS.md section, or paste it into a system prompt.",
71
+ arguments: [],
72
+ flags: [{
73
+ name: "json",
74
+ description: "Print the command registry as JSON instead of Markdown",
75
+ schema: z.boolean().optional()
76
+ }],
77
+ examples: [{
78
+ description: "Write a skill file",
79
+ command: "hardfin agent-guide > SKILL.md"
80
+ }, {
81
+ description: "Inspect the registry",
82
+ command: "hardfin agent-guide --json"
83
+ }],
84
+ run: runAgentGuide
85
+ });
86
+ /** toGuide renders the registry as the Markdown an agent reads. */
87
+ function toGuide(commands, version) {
88
+ const lines = [
89
+ "# Hardfin CLI",
90
+ "",
91
+ `The \`hardfin\` command calls the Hardfin API. This guide describes version ${version} of the CLI, which speaks API version ${API_VERSION}.`,
92
+ "",
93
+ "## Before calling",
94
+ "",
95
+ "- Authenticate by setting `HARDFIN_API_KEY`, or by running `hardfin login`",
96
+ "- Every command prints JSON on stdout and diagnostics on stderr",
97
+ "- Output is JSON whenever stdout is not a terminal, so no flag is needed when calling from code",
98
+ "",
99
+ "## Exit codes",
100
+ "",
101
+ "| Code | Means |",
102
+ "| --- | --- |",
103
+ "| 0 | The command succeeded |",
104
+ "| 1 | The command failed |",
105
+ "| 2 | The command was called wrongly |",
106
+ "| 4 | No usable credential |",
107
+ "",
108
+ "## Commands",
109
+ ""
110
+ ];
111
+ for (const command of commands) {
112
+ lines.push(`### \`hardfin ${command.name}\``, "", command.description ?? command.summary, "");
113
+ if (command.arguments.length > 0) {
114
+ lines.push("| Argument | Required | Holds |", "| --- | --- | --- |");
115
+ for (const argument of command.arguments) lines.push(`| \`${argument.name}\` | ${argument.required ? "yes" : "no"} | ${argument.description} |`);
116
+ lines.push("");
117
+ }
118
+ if (command.flags.length > 0) {
119
+ lines.push("| Flag | Takes | Does |", "| --- | --- | --- |");
120
+ for (const flag of command.flags) {
121
+ const name = flag.short ? `-${flag.short}, --${flag.name}` : `--${flag.name}`;
122
+ lines.push(`| \`${name}\` | ${flag.valueName ?? "nothing"} | ${flag.description} |`);
123
+ }
124
+ lines.push("");
125
+ }
126
+ for (const example of command.examples) lines.push(`${example.description}:`, "", "```sh", example.command, "```", "");
127
+ }
128
+ return lines.join("\n");
129
+ }
130
+ async function runAgentGuide(input) {
131
+ if (input.flags["json"] === true) {
132
+ writeData(input.commands.map(toSummary));
133
+ return ExitCode.OK;
134
+ }
135
+ writeData(toGuide(input.commands, version));
136
+ return ExitCode.OK;
137
+ }
138
+ function toSummary(command) {
139
+ return {
140
+ name: command.name,
141
+ summary: command.summary,
142
+ description: command.description ?? command.summary,
143
+ arguments: command.arguments,
144
+ flags: command.flags.map((flag) => ({
145
+ name: flag.name,
146
+ short: flag.short ?? null,
147
+ description: flag.description,
148
+ valueName: flag.valueName ?? null,
149
+ repeatable: flag.repeatable ?? false
150
+ })),
151
+ examples: command.examples
152
+ };
153
+ }
154
+ //#endregion
155
+ //#region src/http/client.ts
156
+ /** RequestFailure is a call the API refused, carrying what the envelope said. */
157
+ var RequestFailure = class extends Error {
158
+ status;
159
+ errors;
160
+ requestId;
161
+ constructor(status, errors, requestId) {
162
+ super(errors[0]?.error ?? `the request failed with status ${status}`);
163
+ this.name = "RequestFailure";
164
+ this.status = status;
165
+ this.errors = errors;
166
+ this.requestId = requestId;
167
+ }
168
+ };
169
+ /** request calls one /v2 endpoint and returns the envelope it answered with. */
170
+ async function request(options) {
171
+ const url = new URL(`${options.apiUrl}${toLeadingSlash(options.path)}`);
172
+ if (options.query) url.search = options.query.toString();
173
+ const headers = {
174
+ "X-API-Key": options.apiKey,
175
+ "X-API-Version": API_VERSION,
176
+ Accept: "application/json"
177
+ };
178
+ if (options.body !== void 0) headers["Content-Type"] = "application/json";
179
+ const response = await fetch(url, {
180
+ method: options.method,
181
+ headers,
182
+ body: options.body === void 0 ? void 0 : JSON.stringify(options.body)
183
+ });
184
+ const envelope = toEnvelope(await response.text());
185
+ if (!response.ok && envelope === void 0) throw new RequestFailure(response.status, [{
186
+ error: toStatusMessage(response.status),
187
+ statusCode: response.status
188
+ }]);
189
+ if (!response.ok) {
190
+ const errors = envelope?.metadata?.errors ?? [];
191
+ throw new RequestFailure(response.status, errors.length > 0 ? errors : [{
192
+ error: toStatusMessage(response.status),
193
+ statusCode: response.status
194
+ }], envelope?.metadata?.requestId);
195
+ }
196
+ return envelope ?? { data: null };
197
+ }
198
+ function toLeadingSlash(path) {
199
+ return path.startsWith("/") ? path : `/${path}`;
200
+ }
201
+ function toEnvelope(text) {
202
+ if (text.trim() === "") return;
203
+ try {
204
+ return JSON.parse(text);
205
+ } catch {
206
+ return;
207
+ }
208
+ }
209
+ function toStatusMessage(status) {
210
+ if (status === 401) return "the credential is missing or is not valid";
211
+ if (status === 403) return "the organization is deactivated, or its access has ended";
212
+ return `the request failed with status ${status}`;
213
+ }
214
+ const apiCommand = defineCommand({
215
+ name: "api",
216
+ summary: "Call a Hardfin API endpoint and print what it answers",
217
+ description: "Reaches every endpoint the API publishes. The path is everything after /v2, and the response envelope is printed as it arrived.",
218
+ arguments: [{
219
+ name: "path",
220
+ description: "The endpoint path, such as /customer or /item/item_V1StGXR8Z5jdHi6B",
221
+ required: true
222
+ }],
223
+ flags: [
224
+ {
225
+ name: "method",
226
+ short: "X",
227
+ description: "The HTTP method to use",
228
+ valueName: "method",
229
+ schema: z.enum([
230
+ "GET",
231
+ "POST",
232
+ "PATCH",
233
+ "PUT",
234
+ "DELETE"
235
+ ]),
236
+ defaultValue: "GET"
237
+ },
238
+ {
239
+ name: "field",
240
+ short: "f",
241
+ description: "A query parameter as key=value, repeatable",
242
+ valueName: "key=value",
243
+ repeatable: true,
244
+ schema: z.array(z.string()).default([])
245
+ },
246
+ {
247
+ name: "input",
248
+ description: "A file holding the JSON request body, or - for stdin",
249
+ valueName: "file",
250
+ schema: z.string().optional()
251
+ },
252
+ {
253
+ name: "json",
254
+ description: "Print machine-readable output, which is the default when stdout is not a terminal",
255
+ schema: z.boolean().optional()
256
+ }
257
+ ],
258
+ examples: [
259
+ {
260
+ description: "List customers",
261
+ command: "hardfin api /customer"
262
+ },
263
+ {
264
+ description: "Take the second page",
265
+ command: "hardfin api /customer -f page=2 -f limit=50"
266
+ },
267
+ {
268
+ description: "Change an item",
269
+ command: "hardfin api -X PATCH /item/item_V1StGXR8Z5jdHi6B --input body.json"
270
+ }
271
+ ],
272
+ run: runApi
273
+ });
274
+ async function runApi(input) {
275
+ const apiKey = toApiKey();
276
+ if (!apiKey) {
277
+ writeFailure("not authenticated. Set HARDFIN_API_KEY to an API key for your organization", input.isJSON);
278
+ return ExitCode.NOT_AUTHENTICATED;
279
+ }
280
+ const path = input.args[0];
281
+ if (!path) {
282
+ writeFailure("a path is required, such as /customer", input.isJSON);
283
+ return ExitCode.USAGE;
284
+ }
285
+ const query = toQuery(input.flags["field"]);
286
+ if (query === void 0) {
287
+ writeFailure("each --field is key=value, such as -f limit=50", input.isJSON);
288
+ return ExitCode.USAGE;
289
+ }
290
+ let body;
291
+ if (typeof input.flags["input"] === "string") {
292
+ body = toBody(input.flags["input"]);
293
+ if (body === void 0) {
294
+ writeFailure(`${input.flags["input"]} does not hold JSON`, input.isJSON);
295
+ return ExitCode.USAGE;
296
+ }
297
+ }
298
+ try {
299
+ writeData((await request({
300
+ apiUrl: toApiUrl(),
301
+ apiKey,
302
+ method: String(input.flags["method"] ?? "GET").toUpperCase(),
303
+ path,
304
+ query,
305
+ body
306
+ })).data);
307
+ return ExitCode.OK;
308
+ } catch (error) {
309
+ if (error instanceof RequestFailure) {
310
+ writeFailure(error.message, input.isJSON, error.errors, error.requestId);
311
+ return error.status === 401 ? ExitCode.NOT_AUTHENTICATED : ExitCode.ERROR;
312
+ }
313
+ writeFailure(error instanceof Error ? error.message : String(error), input.isJSON);
314
+ return ExitCode.ERROR;
315
+ }
316
+ }
317
+ /** toQuery folds the repeated --field flags into query parameters. */
318
+ function toQuery(fields) {
319
+ const query = new URLSearchParams();
320
+ for (const field of Array.isArray(fields) ? fields : []) {
321
+ const split = field.indexOf("=");
322
+ if (split < 1) return;
323
+ query.append(field.slice(0, split), field.slice(split + 1));
324
+ }
325
+ return query;
326
+ }
327
+ function toBody(source) {
328
+ const text = source === "-" ? readFileSync(0, "utf8") : readFileSync(source, "utf8");
329
+ try {
330
+ return JSON.parse(text);
331
+ } catch {
332
+ return;
333
+ }
334
+ }
335
+ //#endregion
336
+ //#region src/command/commands.ts
337
+ /** commands is every command the CLI offers, and drives help and the agent guide. */
338
+ const commands = [apiCommand, agentGuideCommand];
339
+ //#endregion
340
+ //#region src/cli.ts
341
+ 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();
343
+ for (const command of commands) program.addCommand(toProgram(command));
344
+ await program.parseAsync(process.argv);
345
+ /** toProgram wires one registry command into the parser. */
346
+ function toProgram(command) {
347
+ const program = new Command(command.name).summary(command.summary).description(command.description ?? command.summary);
348
+ for (const argument of command.arguments) {
349
+ const name = argument.variadic ? `${argument.name}...` : argument.name;
350
+ program.argument(argument.required ? `<${name}>` : `[${name}]`, argument.description);
351
+ }
352
+ for (const flag of command.flags) {
353
+ const short = flag.short ? `-${flag.short}, ` : "";
354
+ const value = flag.valueName ? ` <${flag.valueName}>` : "";
355
+ const option = new Option(`${short}--${flag.name}${value}`, flag.description);
356
+ if (flag.repeatable) option.argParser(collect);
357
+ if (flag.defaultValue !== void 0) option.default(flag.defaultValue);
358
+ program.addOption(option);
359
+ }
360
+ for (const example of command.examples) program.addHelpText("after", `\n${example.description}:\n $ ${example.command}`);
361
+ program.action(async (...parsed) => {
362
+ const flags = parsed[parsed.length - 2] ?? {};
363
+ const args = parsed.slice(0, parsed.length - 2).flatMap(toArgumentList);
364
+ process.exitCode = await toExitCode(command, args, flags);
365
+ });
366
+ return program;
367
+ }
368
+ async function toExitCode(command, args, flags) {
369
+ const isJSON = isJSONOutput(flags);
370
+ try {
371
+ return await command.run({
372
+ args,
373
+ flags,
374
+ isJSON,
375
+ commands
376
+ });
377
+ } catch (error) {
378
+ writeFailure(error instanceof Error ? error.message : String(error), isJSON);
379
+ return ExitCode.ERROR;
380
+ }
381
+ }
382
+ function toArgumentList(value) {
383
+ if (Array.isArray(value)) return value.map(String);
384
+ return value === void 0 ? [] : [String(value)];
385
+ }
386
+ function collect(value, previous) {
387
+ return [...previous ?? [], value];
388
+ }
389
+ //#endregion
390
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardfin/cli",
3
- "version": "0.0.2-dev.2",
3
+ "version": "0.0.2-dev.5",
4
4
  "description": "Command line interface for the Hardfin API",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hardfin, Inc.",
@@ -20,15 +20,33 @@
20
20
  ],
21
21
  "type": "module",
22
22
  "bin": {
23
- "hardfin": "bin/hardfin.js"
23
+ "hardfin": "dist/cli.js"
24
24
  },
25
25
  "files": [
26
- "bin"
26
+ "dist"
27
27
  ],
28
28
  "engines": {
29
29
  "node": ">=20"
30
30
  },
31
31
  "publishConfig": {
32
32
  "access": "public"
33
+ },
34
+ "scripts": {
35
+ "build": "tsdown",
36
+ "check-types": "tsc --noEmit",
37
+ "test": "vitest run",
38
+ "test:watch": "vitest",
39
+ "prepublishOnly": "npm run build"
40
+ },
41
+ "dependencies": {
42
+ "commander": "^15.0.0",
43
+ "zod": "^4.6.5"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^22.15.0",
47
+ "tsdown": "^0.23.0",
48
+ "typescript": "^5.9.0",
49
+ "unrun": "^0.3.1",
50
+ "vitest": "^5.0.1"
33
51
  }
34
52
  }
package/bin/hardfin.js DELETED
@@ -1,27 +0,0 @@
1
- #!/usr/bin/env node
2
- // Copyright (c) 2026. Hardfin, Inc. All rights reserved.
3
-
4
- import { createRequire } from "node:module";
5
-
6
- const require = createRequire(import.meta.url);
7
- const { version } = require("../package.json");
8
-
9
- const arg = process.argv[2];
10
-
11
- if (arg === "--version" || arg === "-v" || arg === "version") {
12
- process.stdout.write(`${version}\n`);
13
- process.exit(0);
14
- }
15
-
16
- process.stderr.write(
17
- [
18
- `hardfin ${version}`,
19
- "",
20
- "This release is a placeholder that reserves the package name",
21
- "No commands are available yet",
22
- "",
23
- "https://github.com/hardfinhq/hardfin-cli",
24
- "",
25
- ].join("\n"),
26
- );
27
- process.exit(1);