@hardfin/cli 0.1.1-dev.42 → 0.1.1-dev.45
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 +36 -6
- package/dist/cli.js +9 -6
- package/dist/completion-tree.json +1 -1
- package/dist/{program-CqO1St9H.js → program-Bz-DwZDJ.js} +405 -160
- package/dist/{scrub-BN3gyBpR.js → scrub-BZ0egWxC.js} +4 -4
- package/dist/{sentry-BM9Rvhhy.js → sentry-e3Tum2kz.js} +4 -4
- package/package.json +2 -2
- package/skills/hardfin-cli/SKILL.md +9 -7
- package/templates/AGENTS.md +1 -1
package/README.md
CHANGED
|
@@ -16,6 +16,8 @@ server, and every command also accepts an API key.
|
|
|
16
16
|
npm install -g @hardfin/cli
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
It needs Node 20.12 or later.
|
|
20
|
+
|
|
19
21
|
Previews of unreleased work are published from the `dev` branch.
|
|
20
22
|
|
|
21
23
|
```sh
|
|
@@ -71,6 +73,9 @@ decimal such as `--salvage-value 1500.00` stays a string, which is how the API t
|
|
|
71
73
|
without losing precision.
|
|
72
74
|
|
|
73
75
|
A download is written where you name it, and a terminal is never filled with a file's bytes.
|
|
76
|
+
With no `--output`, the file takes the name it was uploaded under, in the working directory.
|
|
77
|
+
That name never chooses a folder, a hidden file, or a file that is already there. The command
|
|
78
|
+
asks for `--output` in those cases.
|
|
74
79
|
|
|
75
80
|
```sh
|
|
76
81
|
hardfin file get file_7hq2mx9pkr4stz8w --output photo.jpg
|
|
@@ -180,7 +185,9 @@ hardfin logout # forget this machine, and ask Hardfin to revoke it
|
|
|
180
185
|
```
|
|
181
186
|
|
|
182
187
|
An API key is still the way to run unattended. `--api-key`, or `HARDFIN_API_KEY`, wins over
|
|
183
|
-
a stored sign in, so CI and an agent sandbox need no browser. The flag beats the variable.
|
|
188
|
+
a stored sign in, so CI and an agent sandbox need no browser. The flag beats the variable. Prefer the
|
|
189
|
+
variable, because a command line is visible to other programs on the machine and is kept in
|
|
190
|
+
an agent's transcript.
|
|
184
191
|
|
|
185
192
|
An API key is held to two rules before it is sent:
|
|
186
193
|
|
|
@@ -199,7 +206,8 @@ how a local build is set up. Pass `--api-url` to send a key you set to that fold
|
|
|
199
206
|
2. Opens a loopback listener on `127.0.0.1`, over IPv4, on whatever port the machine hands out
|
|
200
207
|
3. Opens the browser to approve this CLI, with a PKCE challenge and a state value
|
|
201
208
|
4. Prints the URL, and waits at the keyboard as well as on the listener
|
|
202
|
-
5. Checks the state and the `iss` it came back with, then trades the code for tokens
|
|
209
|
+
5. Checks the state and the `iss` it came back with, then trades the code for tokens. Every
|
|
210
|
+
endpoint the authorization server named must be https, or this machine
|
|
203
211
|
6. Keeps the refresh token, and nothing else
|
|
204
212
|
|
|
205
213
|
While it waits, the terminal takes two things.
|
|
@@ -216,6 +224,11 @@ and the redirect it failed to follow is still in the address bar.
|
|
|
216
224
|
On WSL the browser is opened through Windows, by `wslview`, then PowerShell, then
|
|
217
225
|
`explorer.exe`, because `xdg-open` reaches nothing outside the distribution.
|
|
218
226
|
|
|
227
|
+
The browser is only ever sent to an https address, or to this machine. The address comes from
|
|
228
|
+
the authorization server, so one with any other scheme is printed for you to read and never
|
|
229
|
+
opened. The listener takes only a response that carries the state this sign in sent, and
|
|
230
|
+
turns any other request away without ending the sign in.
|
|
231
|
+
|
|
219
232
|
The port changes every time, so Hardfin matches a loopback redirect by everything except its
|
|
220
233
|
port, which is what RFC 8252 asks of an authorization server.
|
|
221
234
|
|
|
@@ -401,6 +414,17 @@ exit codes. Each call has these limits:
|
|
|
401
414
|
| Commands running | 4 at once | The next call waits for one to finish |
|
|
402
415
|
| One argument | 128 KiB | Refused, pointing at the `input` argument |
|
|
403
416
|
| Standard input | The call's `input` argument, or nothing | Always closed, so `--input -` never waits |
|
|
417
|
+
| One result | 200,000 characters | Cut, with a line saying it is not complete |
|
|
418
|
+
|
|
419
|
+
A failed call also carries the failure as `structuredContent`, with the same `code`,
|
|
420
|
+
`message`, `errors`, `requestId` and `status` the CLI prints, plus `exitCode`. A file that is
|
|
421
|
+
not text is refused through `--output -`, because handing it over as text would change its
|
|
422
|
+
bytes.
|
|
423
|
+
|
|
424
|
+
Renewing a sign in gives the authorization server 15 seconds for its whole answer. That keeps
|
|
425
|
+
a renewal well inside the 120 seconds a call has, so a slow renewal fails as `timed_out` and
|
|
426
|
+
is not stopped partway. A refresh token the server rotated and never finished sending is
|
|
427
|
+
still lost, which no client can prevent.
|
|
404
428
|
|
|
405
429
|
### What a call may touch on this machine
|
|
406
430
|
|
|
@@ -416,6 +440,9 @@ held to rules a person at a terminal is not.
|
|
|
416
440
|
| Downloads with `--output <path>` | A write, so it runs through `hardfin_write` |
|
|
417
441
|
| Downloads with no `--output` | Refused, naming `--output -` to read the file in place |
|
|
418
442
|
|
|
443
|
+
`hardfin init` is held to the same folder. `hardfin completion install` and `uninstall` are
|
|
444
|
+
refused, along with `login`, `logout` and `mcp`, because a person runs those themselves.
|
|
445
|
+
|
|
419
446
|
The rules cover every flag that takes a path: `--input`, `--file` and `--output`. A person
|
|
420
447
|
runs the command themselves for a file the server will not reach.
|
|
421
448
|
|
|
@@ -472,7 +499,8 @@ Only defects are reported. None of these are:
|
|
|
472
499
|
- a network failure, or a server that cannot be reached
|
|
473
500
|
- a mistake on the command line or in a settings file
|
|
474
501
|
- being signed out, or a sign in that was cancelled or refused
|
|
475
|
-
- the machine refusing a file, such as a full disk or a missing permission
|
|
502
|
+
- the machine refusing a file, such as a full disk or a missing permission, unless the file
|
|
503
|
+
is one the CLI installs, which makes it a broken package
|
|
476
504
|
- anything from a run with no API key and no stored sign in, except a defect in `hardfin login`
|
|
477
505
|
|
|
478
506
|
The last rule is there because package scanners install the CLI and run it in sandboxes. They
|
|
@@ -510,9 +538,11 @@ settings, and the one nearest the top wins.
|
|
|
510
538
|
|
|
511
539
|
An exported variable beats `.env`, because the CLI leaves a variable that is already set alone.
|
|
512
540
|
|
|
513
|
-
An env file sets
|
|
514
|
-
|
|
515
|
-
|
|
541
|
+
An env file sets the four settings: `HARDFIN_API_URL`, `HARDFIN_API_KEY`, `HARDFIN_CLIENT_ID`
|
|
542
|
+
and `HARDFIN_ISSUER_URL`. It may also turn error reports off, with `DO_NOT_TRACK=1` or
|
|
543
|
+
`HARDFIN_TELEMETRY=0`. Anything else in it is ignored, so a `.env` in a folder you cloned
|
|
544
|
+
cannot set `NODE_OPTIONS` for the programs the CLI starts, or send error reports elsewhere
|
|
545
|
+
with `HARDFIN_SENTRY_DSN`. Export a variable yourself to set it.
|
|
516
546
|
|
|
517
547
|
### What a local build writes
|
|
518
548
|
|
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]="
|
|
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]="7a26fe4e-1a9f-538e-af63-a17134f6e803")}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-
|
|
17
|
+
const { toCli, toParseExitCode, wasReportAttempted } = await import("./program-Bz-DwZDJ.js");
|
|
18
18
|
try {
|
|
19
19
|
await toCli().parseAsync(process.argv);
|
|
20
20
|
} catch (error) {
|
|
@@ -31,10 +31,13 @@ try {
|
|
|
31
31
|
async function toCrash(error) {
|
|
32
32
|
if (isCrashing) return;
|
|
33
33
|
isCrashing = true;
|
|
34
|
-
process.
|
|
34
|
+
const isJSON = process.argv.includes("--json") || !process.stdout.isTTY;
|
|
35
35
|
try {
|
|
36
|
-
const { toReported } = await import("./program-
|
|
37
|
-
|
|
36
|
+
const { toReported, writeCrash } = await import("./program-Bz-DwZDJ.js");
|
|
37
|
+
writeCrash(error, isJSON);
|
|
38
|
+
await toReported(error, isJSON, void 0, "fatal");
|
|
39
|
+
} catch {
|
|
40
|
+
process.stderr.write(`error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
38
41
|
} finally {
|
|
39
42
|
process.exit(1);
|
|
40
43
|
}
|
|
@@ -51,4 +54,4 @@ async function toQuickAnswer(args) {
|
|
|
51
54
|
export {};
|
|
52
55
|
|
|
53
56
|
//# sourceMappingURL=cli.js.map
|
|
54
|
-
//# debugId=
|
|
57
|
+
//# debugId=7a26fe4e-1a9f-538e-af63-a17134f6e803
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
{
|
|
9
9
|
"name": "api-key",
|
|
10
|
-
"description": "An API key to call with,
|
|
10
|
+
"description": "An API key to call with. Prefer HARDFIN_API_KEY, because a command line is visible to other programs",
|
|
11
11
|
"valueName": "key"
|
|
12
12
|
},
|
|
13
13
|
{
|
|
@@ -1,7 +1,7 @@
|
|
|
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]="
|
|
4
|
-
import { a as defineCommand, i as GLOBAL_FLAGS, r as ExitCode, t as toInvocation } from "./scrub-
|
|
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]="8bcae06e-d313-56ed-bc3e-49b505487fa3")}catch(e){}}();
|
|
4
|
+
import { a as defineCommand, i as GLOBAL_FLAGS, r as ExitCode, t as toInvocation } from "./scrub-BZ0egWxC.js";
|
|
5
5
|
import { n as toAnswer$1, r as toCompletion, t as PATH_VALUES } from "./completion-tree-DLC6ARLR.js";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { Command, CommanderError, Option } from "commander";
|
|
@@ -9,11 +9,11 @@ import { z } from "zod";
|
|
|
9
9
|
import { accessSync, chmodSync, closeSync, constants, existsSync, mkdirSync, openAsBlob, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
10
10
|
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
11
11
|
import { parseEnv } from "node:util";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
12
13
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
13
14
|
import { arch, cpus, homedir, release, totalmem, type, version } from "node:os";
|
|
14
15
|
import { spawn, spawnSync } from "node:child_process";
|
|
15
16
|
import { createInterface } from "node:readline/promises";
|
|
16
|
-
import { fileURLToPath } from "node:url";
|
|
17
17
|
import { createServer } from "node:http";
|
|
18
18
|
import { createInterface as createInterface$1 } from "node:readline";
|
|
19
19
|
//#region src/output/writer.ts
|
|
@@ -27,10 +27,21 @@ function toText(value) {
|
|
|
27
27
|
if (value === void 0 || value === null) return "";
|
|
28
28
|
return JSON.stringify(value);
|
|
29
29
|
}
|
|
30
|
+
/** Every control character but a newline and a tab, which are the two a line of text needs. */
|
|
31
|
+
const CONTROL = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
|
|
32
|
+
/**
|
|
33
|
+
* toPrintable makes text safe to show at a terminal. A name or a message from the API can
|
|
34
|
+
* hold an escape sequence, which a terminal would act on. Each such character is written
|
|
35
|
+
* as its code, so the text reads as what it holds.
|
|
36
|
+
*/
|
|
37
|
+
function toPrintable(text) {
|
|
38
|
+
return text.replace(CONTROL, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
39
|
+
}
|
|
30
40
|
/** writeData prints a command's result on stdout. */
|
|
31
41
|
function writeData(value) {
|
|
32
42
|
if (typeof value === "string") {
|
|
33
|
-
|
|
43
|
+
const text = toPrintable(value);
|
|
44
|
+
process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
|
|
34
45
|
return;
|
|
35
46
|
}
|
|
36
47
|
process.stdout.write(`${JSON.stringify(value ?? null, null, 2)}\n`);
|
|
@@ -38,9 +49,9 @@ function writeData(value) {
|
|
|
38
49
|
/** writeFailure prints why a command failed on stderr, with a code only a machine reading JSON sees. */
|
|
39
50
|
function writeFailure(code, message, isJSON, errors, requestId, status) {
|
|
40
51
|
if (!isJSON) {
|
|
41
|
-
process.stderr.write(`error: ${message}${status === void 0 ? "" : ` (HTTP ${status})`}\n`);
|
|
42
|
-
for (const entry of errors?.slice(1) ?? []) process.stderr.write(` ${entry.error} (${entry.statusCode})\n`);
|
|
43
|
-
if (requestId) process.stderr.write(` request ${requestId}\n`);
|
|
52
|
+
process.stderr.write(`error: ${toPrintable(message)}${status === void 0 ? "" : ` (HTTP ${status})`}\n`);
|
|
53
|
+
for (const entry of errors?.slice(1) ?? []) process.stderr.write(` ${toPrintable(entry.error)} (${entry.statusCode})\n`);
|
|
54
|
+
if (requestId) process.stderr.write(` request ${toPrintable(requestId)}\n`);
|
|
44
55
|
return;
|
|
45
56
|
}
|
|
46
57
|
const payload = {
|
|
@@ -155,6 +166,77 @@ var TimedOut = class extends ExpectedFailure {
|
|
|
155
166
|
this.name = "TimedOut";
|
|
156
167
|
}
|
|
157
168
|
};
|
|
169
|
+
/** The values that mean no, for a variable that turns something on or off. */
|
|
170
|
+
const OFF = /* @__PURE__ */ new Set([
|
|
171
|
+
"0",
|
|
172
|
+
"false",
|
|
173
|
+
"off",
|
|
174
|
+
"no"
|
|
175
|
+
]);
|
|
176
|
+
/**
|
|
177
|
+
* toTelemetry decides whether defects are reported. A person's opt out wins over everything,
|
|
178
|
+
* and a CLI run from a checkout reports only when its developer asks it to.
|
|
179
|
+
*/
|
|
180
|
+
function toTelemetry(env = process.env, isCheckout = isRunFromCheckout()) {
|
|
181
|
+
const dsn = env["HARDFIN_SENTRY_DSN"] || "https://a4819e652e9fa63c0ab67e9febe44565@o1388102.ingest.us.sentry.io/4512176532684800";
|
|
182
|
+
const base = {
|
|
183
|
+
dsn,
|
|
184
|
+
environment: isCheckout ? "local" : version$1.includes("-dev.") ? "preview" : "production",
|
|
185
|
+
release: toRelease(version$1)
|
|
186
|
+
};
|
|
187
|
+
if (env["DO_NOT_TRACK"] && !OFF.has(env["DO_NOT_TRACK"].toLowerCase())) return {
|
|
188
|
+
...base,
|
|
189
|
+
enabled: false,
|
|
190
|
+
from: "DO_NOT_TRACK"
|
|
191
|
+
};
|
|
192
|
+
const asked = env["HARDFIN_TELEMETRY"]?.toLowerCase();
|
|
193
|
+
if (asked !== void 0 && OFF.has(asked)) return {
|
|
194
|
+
...base,
|
|
195
|
+
enabled: false,
|
|
196
|
+
from: "HARDFIN_TELEMETRY"
|
|
197
|
+
};
|
|
198
|
+
if (!isDsn(dsn)) return {
|
|
199
|
+
...base,
|
|
200
|
+
enabled: false,
|
|
201
|
+
from: "HARDFIN_SENTRY_DSN is not a valid DSN"
|
|
202
|
+
};
|
|
203
|
+
if (asked !== void 0) return {
|
|
204
|
+
...base,
|
|
205
|
+
enabled: true,
|
|
206
|
+
from: "HARDFIN_TELEMETRY"
|
|
207
|
+
};
|
|
208
|
+
return isCheckout ? {
|
|
209
|
+
...base,
|
|
210
|
+
enabled: false,
|
|
211
|
+
from: "run from a checkout"
|
|
212
|
+
} : {
|
|
213
|
+
...base,
|
|
214
|
+
enabled: true,
|
|
215
|
+
from: "default"
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* isRunFromCheckout answers whether this CLI runs from a clone of its repository, as npm link
|
|
220
|
+
* and node dist/cli.js do. The published package holds dist and nothing beside it.
|
|
221
|
+
*/
|
|
222
|
+
function isRunFromCheckout() {
|
|
223
|
+
return existsSync(fileURLToPath(new URL("../src", import.meta.url)));
|
|
224
|
+
}
|
|
225
|
+
/** isDsn answers whether a DSN can be read as the URL of a Sentry project. */
|
|
226
|
+
function isDsn(dsn) {
|
|
227
|
+
try {
|
|
228
|
+
return new URL(dsn).host !== "";
|
|
229
|
+
} catch {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* toRelease names a version as Sentry knows it. Sentry refuses a slash in a release name, so
|
|
235
|
+
* the scoped package name @hardfin/cli cannot be used, and hardfin-cli stands in for it.
|
|
236
|
+
*/
|
|
237
|
+
function toRelease(of) {
|
|
238
|
+
return `hardfin-cli@${of}`;
|
|
239
|
+
}
|
|
158
240
|
//#endregion
|
|
159
241
|
//#region src/config/settings.ts
|
|
160
242
|
/** The API version this build was written against, sent on every request. */
|
|
@@ -225,8 +307,23 @@ function toFileSettings(directory) {
|
|
|
225
307
|
}
|
|
226
308
|
return result.data;
|
|
227
309
|
}
|
|
228
|
-
/** The
|
|
229
|
-
const
|
|
310
|
+
/** The variables an env file may set, which are the settings and nothing else the CLI reads. */
|
|
311
|
+
const SETTING_VARIABLES = /* @__PURE__ */ new Set([
|
|
312
|
+
"HARDFIN_API_URL",
|
|
313
|
+
"HARDFIN_API_KEY",
|
|
314
|
+
"HARDFIN_CLIENT_ID",
|
|
315
|
+
"HARDFIN_ISSUER_URL"
|
|
316
|
+
]);
|
|
317
|
+
/**
|
|
318
|
+
* isTakenFromFile answers whether an env file may set a variable. A file in a folder supplies
|
|
319
|
+
* the settings, and may turn error reports off. Where reports go, where a sign in is kept,
|
|
320
|
+
* and anything a started program reads are for the person to set themselves.
|
|
321
|
+
*/
|
|
322
|
+
function isTakenFromFile(name, value) {
|
|
323
|
+
if (SETTING_VARIABLES.has(name)) return true;
|
|
324
|
+
if (name === "DO_NOT_TRACK") return !OFF.has(value.toLowerCase());
|
|
325
|
+
return name === "HARDFIN_TELEMETRY" && OFF.has(value.toLowerCase());
|
|
326
|
+
}
|
|
230
327
|
/**
|
|
231
328
|
* loadEnvFile reads a .env beside the command, so a local build needs no exports, and
|
|
232
329
|
* answers which variables it supplied. An exported variable is left alone, so a shell
|
|
@@ -243,7 +340,7 @@ function loadEnvFile(directory) {
|
|
|
243
340
|
}
|
|
244
341
|
const supplied = /* @__PURE__ */ new Set();
|
|
245
342
|
for (const [name, value] of Object.entries(held)) {
|
|
246
|
-
if (
|
|
343
|
+
if (value === void 0 || process.env[name] !== void 0 || !isTakenFromFile(name, value)) continue;
|
|
247
344
|
process.env[name] = value;
|
|
248
345
|
supplied.add(name);
|
|
249
346
|
}
|
|
@@ -269,6 +366,19 @@ var Unreachable = class extends ExpectedFailure {
|
|
|
269
366
|
this.name = "Unreachable";
|
|
270
367
|
}
|
|
271
368
|
};
|
|
369
|
+
/** isLoopback reports whether a host is this machine. */
|
|
370
|
+
function isLoopback(hostname) {
|
|
371
|
+
return hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]" || hostname === "localhost";
|
|
372
|
+
}
|
|
373
|
+
/** isSecure answers whether a URL is https, or is this machine, where nothing sits in between. */
|
|
374
|
+
function isSecure(url) {
|
|
375
|
+
try {
|
|
376
|
+
const parsed = new URL(url);
|
|
377
|
+
return parsed.protocol === "https:" || parsed.protocol === "http:" && isLoopback(parsed.hostname);
|
|
378
|
+
} catch {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
272
382
|
const OFFLINE = "Couldn't connect to Hardfin. Check your internet connection and try again.";
|
|
273
383
|
const INSECURE = "Couldn't connect to Hardfin securely, so nothing was sent. A work network or security software may be blocking the connection. If this keeps happening, contact your IT team.";
|
|
274
384
|
/** The reasons Node gives, by the code on the error behind fetch's own. */
|
|
@@ -342,6 +452,12 @@ const REASONS = {
|
|
|
342
452
|
detail: "not an HTTPS server"
|
|
343
453
|
}
|
|
344
454
|
};
|
|
455
|
+
/**
|
|
456
|
+
* How long the authorization server may take over its whole answer. Renewing a sign in comes
|
|
457
|
+
* before the request a command was asked to make, so a slow renewal has to end, and say so,
|
|
458
|
+
* well before a caller's own limit stops the command without a word.
|
|
459
|
+
*/
|
|
460
|
+
const SIGN_IN_TIMEOUT_MS = 15e3;
|
|
345
461
|
/** How long a server may take to begin its answer, before the request is given up on. */
|
|
346
462
|
const ANSWER_TIMEOUT_MS = 6e4;
|
|
347
463
|
/**
|
|
@@ -350,15 +466,34 @@ const ANSWER_TIMEOUT_MS = 6e4;
|
|
|
350
466
|
* would otherwise wait on a stalled connection for as long as it stayed open.
|
|
351
467
|
*/
|
|
352
468
|
async function send(url, init, timeoutMs = ANSWER_TIMEOUT_MS) {
|
|
469
|
+
return await withDeadline(url, init, timeoutMs, (response) => Promise.resolve(response));
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* sendForJSON asks the authorization server, and holds its whole answer to one short deadline.
|
|
473
|
+
* A server that sends its headers and then stalls would otherwise keep a command waiting on
|
|
474
|
+
* the body for as long as the connection stayed open.
|
|
475
|
+
*/
|
|
476
|
+
async function sendForJSON(url, init, timeoutMs = SIGN_IN_TIMEOUT_MS) {
|
|
477
|
+
return await withDeadline(url, init, timeoutMs, async (response, isOver) => {
|
|
478
|
+
return {
|
|
479
|
+
response,
|
|
480
|
+
body: await response.json().catch((error) => {
|
|
481
|
+
if (isOver()) throw error;
|
|
482
|
+
})
|
|
483
|
+
};
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
/** withDeadline makes one request, and gives up on it unless the work on its response ends in time. */
|
|
487
|
+
async function withDeadline(url, init, timeoutMs, work) {
|
|
353
488
|
const deadline = new AbortController();
|
|
354
489
|
const timer = setTimeout(() => {
|
|
355
490
|
deadline.abort();
|
|
356
491
|
}, timeoutMs);
|
|
357
492
|
try {
|
|
358
|
-
return await fetch(url, {
|
|
493
|
+
return await work(await fetch(url, {
|
|
359
494
|
...init,
|
|
360
495
|
signal: init?.signal ? AbortSignal.any([init.signal, deadline.signal]) : deadline.signal
|
|
361
|
-
});
|
|
496
|
+
}), () => deadline.signal.aborted);
|
|
362
497
|
} catch (error) {
|
|
363
498
|
if (deadline.signal.aborted) throw toTimedOut(url, init?.method ?? "GET", timeoutMs);
|
|
364
499
|
throw toUnreachable(url, error);
|
|
@@ -440,11 +575,11 @@ async function revoke(revocationUrl, clientId, token) {
|
|
|
440
575
|
token,
|
|
441
576
|
token_type_hint: "refresh_token"
|
|
442
577
|
})
|
|
443
|
-
});
|
|
578
|
+
}, SIGN_IN_TIMEOUT_MS);
|
|
444
579
|
}
|
|
445
580
|
/** toTokens asks the token endpoint, which answers a flat RFC 6749 body, not the envelope. */
|
|
446
581
|
async function toTokens(url, form) {
|
|
447
|
-
const
|
|
582
|
+
const answered = await sendForJSON(url, {
|
|
448
583
|
method: "POST",
|
|
449
584
|
headers: {
|
|
450
585
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
@@ -452,7 +587,8 @@ async function toTokens(url, form) {
|
|
|
452
587
|
},
|
|
453
588
|
body: new URLSearchParams(form)
|
|
454
589
|
});
|
|
455
|
-
const
|
|
590
|
+
const response = answered.response;
|
|
591
|
+
const body = toRecord$1(answered.body);
|
|
456
592
|
if (!response.ok) throw new GrantFailure(toText(body?.["error"] ?? `http_${response.status}`), body?.["error_description"] === void 0 ? void 0 : toText(body["error_description"]));
|
|
457
593
|
const parsed = TokenResponse.safeParse(body);
|
|
458
594
|
if (!parsed.success) throw new GrantFailure("invalid_response", "the token endpoint did not answer with a token");
|
|
@@ -464,6 +600,10 @@ async function toTokens(url, form) {
|
|
|
464
600
|
expiresAt: parsed.data.expires_in === void 0 ? void 0 : Date.now() + parsed.data.expires_in * 1e3
|
|
465
601
|
};
|
|
466
602
|
}
|
|
603
|
+
/** toRecord reads a body as the object an OAuth answer is, and nothing for any other JSON. */
|
|
604
|
+
function toRecord$1(body) {
|
|
605
|
+
return typeof body === "object" && body !== null && !Array.isArray(body) ? body : void 0;
|
|
606
|
+
}
|
|
467
607
|
//#endregion
|
|
468
608
|
//#region src/credential/store.ts
|
|
469
609
|
const load = createRequire(import.meta.url);
|
|
@@ -622,17 +762,31 @@ async function toMetadata(issuer, signal) {
|
|
|
622
762
|
const cached = toCached(issuer);
|
|
623
763
|
if (cached) return cached;
|
|
624
764
|
const url = `${issuer.replace(/\/+$/, "")}${METADATA_PATH}`;
|
|
625
|
-
const response = await
|
|
765
|
+
const { response, body } = await sendForJSON(url, {
|
|
626
766
|
headers: { Accept: "application/json" },
|
|
627
767
|
signal
|
|
628
768
|
});
|
|
629
769
|
if (!response.ok) throw new DiscoveryFailure(`${url} answered ${response.status}, so this host publishes no authorization server`);
|
|
630
|
-
const parsed = AuthorizationServerMetadata.safeParse(
|
|
770
|
+
const parsed = AuthorizationServerMetadata.safeParse(body);
|
|
631
771
|
if (!parsed.success) throw new DiscoveryFailure(`${url} does not describe an authorization server`);
|
|
632
772
|
if (parsed.data.issuer.replace(/\/+$/, "") !== issuer.replace(/\/+$/, "")) throw new DiscoveryFailure(`${url} names issuer ${parsed.data.issuer}, which is not the host it was read from`);
|
|
773
|
+
const plain = toPlainEndpoint(parsed.data);
|
|
774
|
+
if (plain) throw new DiscoveryFailure(`${url} names ${plain}, which is not https, so nothing is sent to it`);
|
|
633
775
|
keep(issuer, parsed.data);
|
|
634
776
|
return parsed.data;
|
|
635
777
|
}
|
|
778
|
+
/**
|
|
779
|
+
* toPlainEndpoint names an endpoint that would carry a code or a refresh token over a plain
|
|
780
|
+
* connection. The document chooses every endpoint, so each is checked before any is used.
|
|
781
|
+
*/
|
|
782
|
+
function toPlainEndpoint(metadata) {
|
|
783
|
+
return [
|
|
784
|
+
metadata.authorization_endpoint,
|
|
785
|
+
metadata.token_endpoint,
|
|
786
|
+
metadata.revocation_endpoint,
|
|
787
|
+
metadata.device_authorization_endpoint
|
|
788
|
+
].find((endpoint) => endpoint !== void 0 && !isSecure(endpoint));
|
|
789
|
+
}
|
|
636
790
|
/** toCachePath names where one server's document is kept, beside the credentials. */
|
|
637
791
|
function toCachePath(issuer) {
|
|
638
792
|
const name = createHash("sha256").update(issuer).digest("hex").slice(0, 16);
|
|
@@ -645,7 +799,7 @@ function toCached(issuer) {
|
|
|
645
799
|
const held = JSON.parse(readFileSync(path, "utf8"));
|
|
646
800
|
if ((held.readAt ?? 0) + CACHE_MS < Date.now()) return;
|
|
647
801
|
const parsed = AuthorizationServerMetadata.safeParse(held.document);
|
|
648
|
-
return parsed.success ? parsed.data : void 0;
|
|
802
|
+
return parsed.success && !toPlainEndpoint(parsed.data) ? parsed.data : void 0;
|
|
649
803
|
} catch {
|
|
650
804
|
return;
|
|
651
805
|
}
|
|
@@ -899,10 +1053,6 @@ function toHost$1(url) {
|
|
|
899
1053
|
return;
|
|
900
1054
|
}
|
|
901
1055
|
}
|
|
902
|
-
/** isLoopback reports whether a host is this machine. */
|
|
903
|
-
function isLoopback(hostname) {
|
|
904
|
-
return hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]" || hostname === "localhost";
|
|
905
|
-
}
|
|
906
1056
|
/**
|
|
907
1057
|
* toHeldCredential answers a credential without asking the server for one: the API key, or
|
|
908
1058
|
* an access token this process already holds. An error report uses it, because a refresh
|
|
@@ -996,7 +1146,12 @@ async function request$1(options) {
|
|
|
996
1146
|
/** toFileName reads the name a download was offered under, when the server names one. */
|
|
997
1147
|
function toFileName(disposition) {
|
|
998
1148
|
const matched = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(disposition ?? "");
|
|
999
|
-
|
|
1149
|
+
if (!matched?.[1]) return;
|
|
1150
|
+
try {
|
|
1151
|
+
return decodeURIComponent(matched[1]);
|
|
1152
|
+
} catch {
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1000
1155
|
}
|
|
1001
1156
|
function toLeadingSlash(path) {
|
|
1002
1157
|
return path.startsWith("/") ? path : `/${path}`;
|
|
@@ -1016,6 +1171,35 @@ function toStatusMessage(status) {
|
|
|
1016
1171
|
return `the request failed with status ${status}`;
|
|
1017
1172
|
}
|
|
1018
1173
|
//#endregion
|
|
1174
|
+
//#region src/system/package.ts
|
|
1175
|
+
/** The folders this package ships, beside its manifest. */
|
|
1176
|
+
const SHIPPED = [
|
|
1177
|
+
"dist",
|
|
1178
|
+
"skills",
|
|
1179
|
+
"templates"
|
|
1180
|
+
];
|
|
1181
|
+
/** toPackageDirectory finds this package's own folder, which holds the skills and templates it ships. */
|
|
1182
|
+
function toPackageDirectory(from = dirname(fileURLToPath(import.meta.url))) {
|
|
1183
|
+
for (let directory = from;; directory = dirname(directory)) {
|
|
1184
|
+
const manifest = join(directory, "package.json");
|
|
1185
|
+
if (existsSync(manifest) && JSON.parse(readFileSync(manifest, "utf8")).name === "@hardfin/cli") return directory;
|
|
1186
|
+
if (dirname(directory) === directory) throw new Error("the @hardfin/cli package folder could not be found, so its skills cannot be copied");
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* isShipped answers whether a path is one of the files this package installs. A file of the
|
|
1191
|
+
* CLI's own that cannot be read is a broken install, not the person's machine refusing them.
|
|
1192
|
+
*/
|
|
1193
|
+
function isShipped(path, packageDirectory) {
|
|
1194
|
+
let directory;
|
|
1195
|
+
try {
|
|
1196
|
+
directory = packageDirectory ?? toPackageDirectory();
|
|
1197
|
+
} catch {
|
|
1198
|
+
return false;
|
|
1199
|
+
}
|
|
1200
|
+
return path === join(directory, "package.json") || SHIPPED.some((folder) => path === join(directory, folder) || path.startsWith(join(directory, folder) + sep));
|
|
1201
|
+
}
|
|
1202
|
+
//#endregion
|
|
1019
1203
|
//#region src/report/classify.ts
|
|
1020
1204
|
/**
|
|
1021
1205
|
* The codes Node gives when the machine refuses something: permissions, a full or read-only
|
|
@@ -1042,8 +1226,9 @@ function isReportable(error) {
|
|
|
1042
1226
|
}
|
|
1043
1227
|
/** isMachineRefusal answers whether an error is the machine refusing a file or a folder. */
|
|
1044
1228
|
function isMachineRefusal(error) {
|
|
1045
|
-
const code = error instanceof Error ? error
|
|
1046
|
-
|
|
1229
|
+
const { code, path } = error instanceof Error ? error : {};
|
|
1230
|
+
if (typeof code !== "string" || !ENVIRONMENT_CODES.has(code)) return false;
|
|
1231
|
+
return !(typeof path === "string" && isShipped(path));
|
|
1047
1232
|
}
|
|
1048
1233
|
//#endregion
|
|
1049
1234
|
//#region src/output/failure.ts
|
|
@@ -1135,7 +1320,7 @@ function toGuide(commands, version) {
|
|
|
1135
1320
|
"",
|
|
1136
1321
|
"## Before calling",
|
|
1137
1322
|
"",
|
|
1138
|
-
"- Authenticate
|
|
1323
|
+
"- Authenticate by running `hardfin login`, or by setting `HARDFIN_API_KEY` in the environment. Do not pass a key as `--api-key`, because a command line is kept in transcripts and shown to other programs",
|
|
1139
1324
|
"- `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`",
|
|
1140
1325
|
"- Every command prints JSON on stdout and diagnostics on stderr",
|
|
1141
1326
|
"- Output is JSON whenever stdout is not a terminal, so no flag is needed when calling from code",
|
|
@@ -1193,7 +1378,7 @@ function toCommandLines(command, parents) {
|
|
|
1193
1378
|
}
|
|
1194
1379
|
async function runAgentGuide(input) {
|
|
1195
1380
|
if (input.flags["json"] === true) {
|
|
1196
|
-
writeData(input.commands.map(toSummary));
|
|
1381
|
+
writeData(input.commands.filter((command) => !command.hidden).map(toSummary));
|
|
1197
1382
|
return ExitCode.OK;
|
|
1198
1383
|
}
|
|
1199
1384
|
writeData(toGuide(input.commands, version$1));
|
|
@@ -1374,26 +1559,41 @@ function isWSL() {
|
|
|
1374
1559
|
return existsSync("/proc/sys/fs/binfmt_misc/WSLInterop");
|
|
1375
1560
|
}
|
|
1376
1561
|
/**
|
|
1377
|
-
*
|
|
1562
|
+
* toOpenable answers the address a browser may be sent to, and nothing for anything else. The
|
|
1563
|
+
* address comes from the authorization server, which a file in the working directory can
|
|
1564
|
+
* name, so it is parsed and written out again before any program is handed it.
|
|
1565
|
+
*/
|
|
1566
|
+
function toOpenable(url) {
|
|
1567
|
+
let parsed;
|
|
1568
|
+
try {
|
|
1569
|
+
parsed = new URL(url);
|
|
1570
|
+
} catch {
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
const isLocal = parsed.protocol === "http:" && LOOPBACK_HOSTS.has(parsed.hostname);
|
|
1574
|
+
return parsed.protocol === "https:" || isLocal ? parsed.href : void 0;
|
|
1575
|
+
}
|
|
1576
|
+
const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
1577
|
+
"127.0.0.1",
|
|
1578
|
+
"[::1]",
|
|
1579
|
+
"localhost"
|
|
1580
|
+
]);
|
|
1581
|
+
/**
|
|
1582
|
+
* toOpenerList lists the ways to open a URL here, best first. WSL needs Windows to do it,
|
|
1378
1583
|
* because the browser the person is looking at is the Windows one, and xdg-open reaches
|
|
1379
1584
|
* nothing outside the distribution.
|
|
1380
1585
|
*/
|
|
1381
|
-
function
|
|
1382
|
-
if (
|
|
1383
|
-
if (
|
|
1384
|
-
|
|
1385
|
-
"start",
|
|
1386
|
-
"",
|
|
1387
|
-
url
|
|
1388
|
-
]]];
|
|
1389
|
-
if (isWSL()) return [
|
|
1586
|
+
function toOpenerList(url, platform = process.platform, isInsideWSL = isWSL()) {
|
|
1587
|
+
if (platform === "darwin") return [["open", [url]]];
|
|
1588
|
+
if (platform === "win32") return [["rundll32.exe", ["url.dll,FileProtocolHandler", url]]];
|
|
1589
|
+
if (isInsideWSL) return [
|
|
1390
1590
|
["wslview", [url]],
|
|
1391
1591
|
["powershell.exe", [
|
|
1392
1592
|
"-NoProfile",
|
|
1393
1593
|
"-NonInteractive",
|
|
1394
1594
|
"-Command",
|
|
1395
1595
|
"Start-Process",
|
|
1396
|
-
`'${url}'`
|
|
1596
|
+
`'${url.replaceAll("'", "''")}'`
|
|
1397
1597
|
]],
|
|
1398
1598
|
["explorer.exe", [url]]
|
|
1399
1599
|
];
|
|
@@ -1401,7 +1601,9 @@ function toOpeners(url) {
|
|
|
1401
1601
|
}
|
|
1402
1602
|
/** openBrowser asks the desktop to show a URL, and reports whether anything took it. */
|
|
1403
1603
|
function openBrowser(url, launch = spawnSync) {
|
|
1404
|
-
|
|
1604
|
+
const openable = toOpenable(url);
|
|
1605
|
+
if (openable === void 0) return false;
|
|
1606
|
+
for (const [command, args] of toOpenerList(openable)) if (launch(command, args, {
|
|
1405
1607
|
stdio: "ignore",
|
|
1406
1608
|
cwd: command.endsWith(".exe") ? "/mnt/c" : void 0,
|
|
1407
1609
|
timeout: 1e4
|
|
@@ -2161,14 +2363,6 @@ function toFiles(directory) {
|
|
|
2161
2363
|
return statSync(path).isDirectory() ? toFiles(path) : [path];
|
|
2162
2364
|
});
|
|
2163
2365
|
}
|
|
2164
|
-
/** toPackageDirectory finds this package's own folder, which holds the skills and templates it ships. */
|
|
2165
|
-
function toPackageDirectory(from = dirname(fileURLToPath(import.meta.url))) {
|
|
2166
|
-
for (let directory = from;; directory = dirname(directory)) {
|
|
2167
|
-
const manifest = join(directory, "package.json");
|
|
2168
|
-
if (existsSync(manifest) && JSON.parse(readFileSync(manifest, "utf8")).name === "@hardfin/cli") return directory;
|
|
2169
|
-
if (dirname(directory) === directory) throw new Error("the @hardfin/cli package folder could not be found, so its skills cannot be copied");
|
|
2170
|
-
}
|
|
2171
|
-
}
|
|
2172
2366
|
//#endregion
|
|
2173
2367
|
//#region src/command/types.generated.ts
|
|
2174
2368
|
/** API_TYPES is the TypeScript hardfin types prints. */
|
|
@@ -2229,7 +2423,7 @@ const DeviceResponse = z.looseObject({
|
|
|
2229
2423
|
});
|
|
2230
2424
|
/** toDeviceAuthorization asks for a code a person can type on another machine. */
|
|
2231
2425
|
async function toDeviceAuthorization(endpoint, clientId, scope) {
|
|
2232
|
-
const
|
|
2426
|
+
const answered = await sendForJSON(endpoint, {
|
|
2233
2427
|
method: "POST",
|
|
2234
2428
|
headers: {
|
|
2235
2429
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
@@ -2240,7 +2434,8 @@ async function toDeviceAuthorization(endpoint, clientId, scope) {
|
|
|
2240
2434
|
scope
|
|
2241
2435
|
})
|
|
2242
2436
|
});
|
|
2243
|
-
const
|
|
2437
|
+
const response = answered.response;
|
|
2438
|
+
const body = toRecord$1(answered.body);
|
|
2244
2439
|
if (!response.ok) throw new GrantFailure(toText(body?.["error"] ?? `http_${response.status}`), body?.["error_description"] === void 0 ? void 0 : toText(body["error_description"]));
|
|
2245
2440
|
const parsed = DeviceResponse.safeParse(body);
|
|
2246
2441
|
if (!parsed.success) throw new GrantFailure("invalid_response", "the device endpoint did not answer with a code");
|
|
@@ -2291,7 +2486,7 @@ const HOST = "127.0.0.1";
|
|
|
2291
2486
|
* the machine hands out, which is why the server matches a loopback redirect by everything
|
|
2292
2487
|
* except its port.
|
|
2293
2488
|
*/
|
|
2294
|
-
async function toListener(timeoutMs) {
|
|
2489
|
+
async function toListener(timeoutMs, state) {
|
|
2295
2490
|
let settle = () => {};
|
|
2296
2491
|
let fail = () => {};
|
|
2297
2492
|
const callback = new Promise((resolve, reject) => {
|
|
@@ -2304,7 +2499,11 @@ async function toListener(timeoutMs) {
|
|
|
2304
2499
|
response.writeHead(404).end();
|
|
2305
2500
|
return;
|
|
2306
2501
|
}
|
|
2307
|
-
|
|
2502
|
+
if (!isOurs(answered, state)) {
|
|
2503
|
+
writePage(response, 400, "Not this sign in", "This page did not come from the sign in your terminal started.");
|
|
2504
|
+
return;
|
|
2505
|
+
}
|
|
2506
|
+
writeOutcome(response, answered);
|
|
2308
2507
|
settle(answered);
|
|
2309
2508
|
});
|
|
2310
2509
|
await new Promise((resolve, reject) => {
|
|
@@ -2337,10 +2536,33 @@ function toCallback(request) {
|
|
|
2337
2536
|
errorDescription: url.searchParams.get("error_description") ?? void 0
|
|
2338
2537
|
};
|
|
2339
2538
|
}
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2539
|
+
/**
|
|
2540
|
+
* isOurs answers whether a response belongs to this sign in. A refusal with no state at all
|
|
2541
|
+
* is still taken, because not every authorization server sends the state back with one.
|
|
2542
|
+
*/
|
|
2543
|
+
function isOurs(callback, state) {
|
|
2544
|
+
if (callback.state !== void 0) return callback.state === state;
|
|
2545
|
+
return callback.code === void 0 && callback.error !== void 0;
|
|
2546
|
+
}
|
|
2547
|
+
function writeOutcome(response, callback) {
|
|
2548
|
+
if (!callback.error) {
|
|
2549
|
+
writePage(response, 200, "Signed in", "You can close this tab and return to your terminal.");
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
writePage(response, 200, "Sign in refused", `${callback.error}${callback.errorDescription ? `: ${callback.errorDescription}` : ""}`);
|
|
2553
|
+
}
|
|
2554
|
+
/** toEscaped makes text safe to put in a page, because the words of a refusal come from the URL. */
|
|
2555
|
+
function toEscaped(text) {
|
|
2556
|
+
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
2557
|
+
}
|
|
2558
|
+
function writePage(response, status, heading, words) {
|
|
2559
|
+
const title = toEscaped(heading);
|
|
2560
|
+
const detail = toEscaped(words);
|
|
2561
|
+
response.writeHead(status, {
|
|
2562
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
2563
|
+
"Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'",
|
|
2564
|
+
"X-Content-Type-Options": "nosniff"
|
|
2565
|
+
}).end(`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${title}</title></head><body style="font-family: system-ui, sans-serif; padding: 3rem; max-width: 34rem"><h1 style="font-size: 1.25rem">${title}</h1><p>${detail}</p></body></html>`);
|
|
2344
2566
|
}
|
|
2345
2567
|
function close(server, timer) {
|
|
2346
2568
|
clearTimeout(timer);
|
|
@@ -2528,9 +2750,9 @@ async function runLogin(input) {
|
|
|
2528
2750
|
const metadata = await toMetadata(settings.issuerUrl);
|
|
2529
2751
|
const scope = toText(input.flags["scope"] ?? DEFAULT_SCOPES);
|
|
2530
2752
|
if (input.flags["device"] === true) return await runDeviceLogin(input, metadata, scope);
|
|
2531
|
-
const listener = await toListener(WAIT_MS);
|
|
2532
|
-
const pkce = toPkce();
|
|
2533
2753
|
const state = toState();
|
|
2754
|
+
const listener = await toListener(WAIT_MS, state);
|
|
2755
|
+
const pkce = toPkce();
|
|
2534
2756
|
const url = toAuthorizationUrl(metadata.authorization_endpoint, {
|
|
2535
2757
|
clientId,
|
|
2536
2758
|
redirectUri: listener.redirectUri,
|
|
@@ -2601,8 +2823,8 @@ async function runDeviceLogin(input, metadata, scope) {
|
|
|
2601
2823
|
});
|
|
2602
2824
|
else process.stderr.write([
|
|
2603
2825
|
"",
|
|
2604
|
-
` Code ${device.userCode}`,
|
|
2605
|
-
` At ${device.verificationUri}`,
|
|
2826
|
+
` Code ${toPrintable(device.userCode)}`,
|
|
2827
|
+
` At ${toPrintable(device.verificationUri)}`,
|
|
2606
2828
|
"",
|
|
2607
2829
|
opened ? "Opened your browser there. Waiting for approval\n" : "Open that page on any machine, and enter the code. Waiting for approval\n"
|
|
2608
2830
|
].join("\n"));
|
|
@@ -2787,7 +3009,7 @@ function defineOperation(operation) {
|
|
|
2787
3009
|
async function runOperation(operation, input) {
|
|
2788
3010
|
const path = toPath(operation, input.args);
|
|
2789
3011
|
if (path === void 0) {
|
|
2790
|
-
writeFailure("invalid_arguments", `this command takes ${operation.pathParameters.length} argument(s)`, input.isJSON);
|
|
3012
|
+
writeFailure("invalid_arguments", `this command takes ${operation.pathParameters.length} argument(s), each the key of a record`, input.isJSON);
|
|
2791
3013
|
return ExitCode.USAGE;
|
|
2792
3014
|
}
|
|
2793
3015
|
const given = toGivenBody(input.flags["input"]);
|
|
@@ -2850,7 +3072,12 @@ async function runOperation(operation, input) {
|
|
|
2850
3072
|
*/
|
|
2851
3073
|
function toWritten(download, input) {
|
|
2852
3074
|
const asked = input.flags["output"];
|
|
2853
|
-
const
|
|
3075
|
+
const offered = typeof asked === "string" ? void 0 : download.fileName;
|
|
3076
|
+
const path = typeof asked === "string" ? asked : offered === void 0 ? "-" : toLocalName(offered);
|
|
3077
|
+
if (path === void 0 || offered !== void 0 && existsSync(path)) {
|
|
3078
|
+
writeFailure("output_required", path === void 0 ? `this file is offered as ${JSON.stringify(offered)}, which is not a name to write here, so name where to write it with --output` : `${path} already exists, so name where to write this file with --output`, input.isJSON);
|
|
3079
|
+
return ExitCode.USAGE;
|
|
3080
|
+
}
|
|
2854
3081
|
if (path === "-") {
|
|
2855
3082
|
if (process.stdout.isTTY) {
|
|
2856
3083
|
writeFailure("output_required", "this answer is a file, so name where to write it with --output, or send it on with a pipe", input.isJSON);
|
|
@@ -2867,9 +3094,20 @@ function toWritten(download, input) {
|
|
|
2867
3094
|
});
|
|
2868
3095
|
return ExitCode.OK;
|
|
2869
3096
|
}
|
|
3097
|
+
/** toLocalName reduces a name the API offered to one file in the working directory, or to nothing. */
|
|
3098
|
+
function toLocalName(offered) {
|
|
3099
|
+
const name = offered.split(/[\\/]/).pop() ?? "";
|
|
3100
|
+
return name === "" || name.startsWith(".") ? void 0 : name;
|
|
3101
|
+
}
|
|
3102
|
+
/** The arguments a URL reads as "this folder" and "the one above", and one that names nothing. */
|
|
3103
|
+
const NOT_A_KEY = /* @__PURE__ */ new Set([
|
|
3104
|
+
"",
|
|
3105
|
+
".",
|
|
3106
|
+
".."
|
|
3107
|
+
]);
|
|
2870
3108
|
/** toPath fills the path template from the positional arguments, in order. */
|
|
2871
3109
|
function toPath(operation, args) {
|
|
2872
|
-
if (args.length !== operation.pathParameters.length) return;
|
|
3110
|
+
if (args.length !== operation.pathParameters.length || args.some((argument) => NOT_A_KEY.has(argument))) return;
|
|
2873
3111
|
let path = operation.path;
|
|
2874
3112
|
for (const [index, parameter] of operation.pathParameters.entries()) path = path.replace(`{${parameter.name}}`, encodeURIComponent(args[index] ?? ""));
|
|
2875
3113
|
return path;
|
|
@@ -3007,6 +3245,10 @@ function toRefusedPath(flag, path, isWritten, root = process.cwd()) {
|
|
|
3007
3245
|
if (isWithin(toReal(dirname(toCredentialPath())), target)) return refuse("it is where the sign in is kept");
|
|
3008
3246
|
if (isWritten && existsSync(target)) return refuse("it already exists, and a call does not overwrite a file");
|
|
3009
3247
|
}
|
|
3248
|
+
/** isInsideFolder answers whether a path is the folder the server runs in, or sits inside it. */
|
|
3249
|
+
function isInsideFolder(path, root = process.cwd()) {
|
|
3250
|
+
return isWithin(toReal(root), toReal(resolve(root, path)));
|
|
3251
|
+
}
|
|
3010
3252
|
/** toEnvFile names the env file a person pointed the CLI at, when they did. */
|
|
3011
3253
|
function toEnvFile(root) {
|
|
3012
3254
|
const named = process.env["HARDFIN_ENV_FILE"];
|
|
@@ -3026,76 +3268,6 @@ function toReal(path) {
|
|
|
3026
3268
|
const parent = dirname(path);
|
|
3027
3269
|
return parent === path ? path : join(toReal(parent), basename(path));
|
|
3028
3270
|
}
|
|
3029
|
-
const OFF = /* @__PURE__ */ new Set([
|
|
3030
|
-
"0",
|
|
3031
|
-
"false",
|
|
3032
|
-
"off",
|
|
3033
|
-
"no"
|
|
3034
|
-
]);
|
|
3035
|
-
/**
|
|
3036
|
-
* toTelemetry decides whether defects are reported. A person's opt out wins over everything,
|
|
3037
|
-
* and a CLI run from a checkout reports only when its developer asks it to.
|
|
3038
|
-
*/
|
|
3039
|
-
function toTelemetry(env = process.env, isCheckout = isRunFromCheckout()) {
|
|
3040
|
-
const dsn = env["HARDFIN_SENTRY_DSN"] || "https://a4819e652e9fa63c0ab67e9febe44565@o1388102.ingest.us.sentry.io/4512176532684800";
|
|
3041
|
-
const base = {
|
|
3042
|
-
dsn,
|
|
3043
|
-
environment: isCheckout ? "local" : version$1.includes("-dev.") ? "preview" : "production",
|
|
3044
|
-
release: toRelease(version$1)
|
|
3045
|
-
};
|
|
3046
|
-
if (env["DO_NOT_TRACK"] && !OFF.has(env["DO_NOT_TRACK"].toLowerCase())) return {
|
|
3047
|
-
...base,
|
|
3048
|
-
enabled: false,
|
|
3049
|
-
from: "DO_NOT_TRACK"
|
|
3050
|
-
};
|
|
3051
|
-
const asked = env["HARDFIN_TELEMETRY"]?.toLowerCase();
|
|
3052
|
-
if (asked !== void 0 && OFF.has(asked)) return {
|
|
3053
|
-
...base,
|
|
3054
|
-
enabled: false,
|
|
3055
|
-
from: "HARDFIN_TELEMETRY"
|
|
3056
|
-
};
|
|
3057
|
-
if (!isDsn(dsn)) return {
|
|
3058
|
-
...base,
|
|
3059
|
-
enabled: false,
|
|
3060
|
-
from: "HARDFIN_SENTRY_DSN is not a valid DSN"
|
|
3061
|
-
};
|
|
3062
|
-
if (asked !== void 0) return {
|
|
3063
|
-
...base,
|
|
3064
|
-
enabled: true,
|
|
3065
|
-
from: "HARDFIN_TELEMETRY"
|
|
3066
|
-
};
|
|
3067
|
-
return isCheckout ? {
|
|
3068
|
-
...base,
|
|
3069
|
-
enabled: false,
|
|
3070
|
-
from: "run from a checkout"
|
|
3071
|
-
} : {
|
|
3072
|
-
...base,
|
|
3073
|
-
enabled: true,
|
|
3074
|
-
from: "default"
|
|
3075
|
-
};
|
|
3076
|
-
}
|
|
3077
|
-
/**
|
|
3078
|
-
* isRunFromCheckout answers whether this CLI runs from a clone of its repository, as npm link
|
|
3079
|
-
* and node dist/cli.js do. The published package holds dist and nothing beside it.
|
|
3080
|
-
*/
|
|
3081
|
-
function isRunFromCheckout() {
|
|
3082
|
-
return existsSync(fileURLToPath(new URL("../src", import.meta.url)));
|
|
3083
|
-
}
|
|
3084
|
-
/** isDsn answers whether a DSN can be read as the URL of a Sentry project. */
|
|
3085
|
-
function isDsn(dsn) {
|
|
3086
|
-
try {
|
|
3087
|
-
return new URL(dsn).host !== "";
|
|
3088
|
-
} catch {
|
|
3089
|
-
return false;
|
|
3090
|
-
}
|
|
3091
|
-
}
|
|
3092
|
-
/**
|
|
3093
|
-
* toRelease names a version as Sentry knows it. Sentry refuses a slash in a release name, so
|
|
3094
|
-
* the scoped package name @hardfin/cli cannot be used, and hardfin-cli stands in for it.
|
|
3095
|
-
*/
|
|
3096
|
-
function toRelease(of) {
|
|
3097
|
-
return `hardfin-cli@${of}`;
|
|
3098
|
-
}
|
|
3099
3271
|
//#endregion
|
|
3100
3272
|
//#region src/system/host.ts
|
|
3101
3273
|
const BYTES_PER_GB = 1024 ** 3;
|
|
@@ -3402,7 +3574,7 @@ async function report(error, occasion, resolved) {
|
|
|
3402
3574
|
try {
|
|
3403
3575
|
const status = settings ? await toStatus(settings) : void 0;
|
|
3404
3576
|
const secrets = [settings?.settings.apiKey, settings ? toCredential(settings.settings.issuerUrl)?.refreshToken : void 0];
|
|
3405
|
-
return await (await import("./sentry-
|
|
3577
|
+
return await (await import("./sentry-e3Tum2kz.js")).send({
|
|
3406
3578
|
error,
|
|
3407
3579
|
occasion,
|
|
3408
3580
|
telemetry,
|
|
@@ -3621,7 +3793,7 @@ function toResource(uri, commands) {
|
|
|
3621
3793
|
}
|
|
3622
3794
|
/** toTree names every command and what it takes, without the schemas a tool list would carry. */
|
|
3623
3795
|
function toTree(commands) {
|
|
3624
|
-
return commands.map((command) => ({
|
|
3796
|
+
return commands.filter((command) => !command.hidden).map((command) => ({
|
|
3625
3797
|
name: command.name,
|
|
3626
3798
|
summary: command.summary,
|
|
3627
3799
|
arguments: command.arguments.map((argument) => argument.name),
|
|
@@ -3629,11 +3801,16 @@ function toTree(commands) {
|
|
|
3629
3801
|
subcommands: command.subcommands ? toTree(command.subcommands) : void 0
|
|
3630
3802
|
}));
|
|
3631
3803
|
}
|
|
3632
|
-
/**
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3804
|
+
/**
|
|
3805
|
+
* The commands a person runs themselves, each by the words that run it. Signing in needs a
|
|
3806
|
+
* browser and a person, and installing completion changes the startup file of their shell.
|
|
3807
|
+
*/
|
|
3808
|
+
const REFUSED_COMMANDS = /* @__PURE__ */ new Map([
|
|
3809
|
+
["login", "login"],
|
|
3810
|
+
["logout", "logout"],
|
|
3811
|
+
["mcp", "mcp"],
|
|
3812
|
+
["install", "completion install"],
|
|
3813
|
+
["uninstall", "completion uninstall"]
|
|
3637
3814
|
]);
|
|
3638
3815
|
/**
|
|
3639
3816
|
* toParsed asks the CLI's own parser what a call would run, without running it. The server
|
|
@@ -3672,7 +3849,8 @@ function toFixedOption(parsed) {
|
|
|
3672
3849
|
*/
|
|
3673
3850
|
function toRefusedFile(parsed) {
|
|
3674
3851
|
if (!parsed.ran) return;
|
|
3675
|
-
const { command, flags } = parsed.ran;
|
|
3852
|
+
const { command, args, flags } = parsed.ran;
|
|
3853
|
+
if (command.name === "init" && args[0] !== void 0 && !isInsideFolder(args[0])) return `init cannot set up ${args[0]}, because it is outside the folder this server runs in. A person can run hardfin init there themselves`;
|
|
3676
3854
|
for (const flag of command.flags.filter((entry) => PATH_VALUES.has(entry.valueName ?? ""))) {
|
|
3677
3855
|
const path = flags[toOptionKey(flag.name)];
|
|
3678
3856
|
const isWritten = flag.name === "output";
|
|
@@ -3697,18 +3875,44 @@ async function toToolResult(request, commands, run) {
|
|
|
3697
3875
|
if (fixed) return toError(fixed);
|
|
3698
3876
|
const confined = toRefusedFile(parsed);
|
|
3699
3877
|
if (confined) return toError(confined);
|
|
3700
|
-
const command = parsed.ran?.command.name ?? "";
|
|
3701
|
-
if (
|
|
3878
|
+
const command = REFUSED_COMMANDS.get(parsed.ran?.command.name ?? "");
|
|
3879
|
+
if (command !== void 0) return toError(`${command} is run by a person at a terminal, not through this server. Run hardfin ${command} yourself, then call this tool again`);
|
|
3702
3880
|
if (name === READ_TOOL.name && toAccessOf(parsed) === "write") return toError(`this command changes data, so it runs through ${WRITE_TOOL.name}`);
|
|
3703
3881
|
const outcome = await run(asked, called?.input);
|
|
3882
|
+
const text = toBounded([outcome.stdout, outcome.stderr].filter((part) => part.trim() !== "").join("\n"));
|
|
3883
|
+
const failure = outcome.code === 0 ? void 0 : toFailure(outcome);
|
|
3704
3884
|
return {
|
|
3705
3885
|
content: [{
|
|
3706
3886
|
type: "text",
|
|
3707
|
-
text:
|
|
3887
|
+
text: text || `hardfin exited ${outcome.code}`
|
|
3708
3888
|
}],
|
|
3709
|
-
isError: outcome.code !== 0
|
|
3889
|
+
isError: outcome.code !== 0,
|
|
3890
|
+
...failure ? { structuredContent: failure } : {}
|
|
3710
3891
|
};
|
|
3711
3892
|
}
|
|
3893
|
+
/** The most text one result carries, because every character of it sits in the agent's context. */
|
|
3894
|
+
const RESULT_CHARACTERS = 2e5;
|
|
3895
|
+
/** toBounded cuts a result that is too long to be useful, and says that it did. */
|
|
3896
|
+
function toBounded(text, limit = RESULT_CHARACTERS) {
|
|
3897
|
+
if (text.length <= limit) return text;
|
|
3898
|
+
return `${text.slice(0, limit)}\n\n[This result was cut at ${limit} of ${text.length} characters, so it is not complete and may not parse. Ask for less, with --limit, --page or a filter]`;
|
|
3899
|
+
}
|
|
3900
|
+
/**
|
|
3901
|
+
* toFailure reads the failure a command printed, so a client can branch on its code without
|
|
3902
|
+
* parsing text. A command that printed anything else beside it answers with the text alone.
|
|
3903
|
+
*/
|
|
3904
|
+
function toFailure(outcome) {
|
|
3905
|
+
try {
|
|
3906
|
+
const parsed = JSON.parse(outcome.stderr);
|
|
3907
|
+
if (typeof parsed !== "object" || parsed === null || typeof parsed.code !== "string") return;
|
|
3908
|
+
return {
|
|
3909
|
+
...parsed,
|
|
3910
|
+
exitCode: outcome.code
|
|
3911
|
+
};
|
|
3912
|
+
} catch {
|
|
3913
|
+
return;
|
|
3914
|
+
}
|
|
3915
|
+
}
|
|
3712
3916
|
function toError(text) {
|
|
3713
3917
|
return {
|
|
3714
3918
|
content: [{
|
|
@@ -3792,35 +3996,72 @@ function toRun(entry, args, input, timeoutMs) {
|
|
|
3792
3996
|
});
|
|
3793
3997
|
return;
|
|
3794
3998
|
}
|
|
3795
|
-
|
|
3796
|
-
|
|
3999
|
+
const stdout = toCollector();
|
|
4000
|
+
const stderr = toCollector();
|
|
3797
4001
|
let isTimedOut = false;
|
|
3798
4002
|
const timer = setTimeout(() => {
|
|
3799
4003
|
isTimedOut = true;
|
|
3800
4004
|
child.kill("SIGKILL");
|
|
3801
4005
|
}, timeoutMs);
|
|
3802
|
-
child.stdout.on("data",
|
|
3803
|
-
|
|
3804
|
-
});
|
|
3805
|
-
child.stderr.on("data", (chunk) => {
|
|
3806
|
-
stderr += chunk.toString();
|
|
3807
|
-
});
|
|
4006
|
+
child.stdout.on("data", stdout.take);
|
|
4007
|
+
child.stderr.on("data", stderr.take);
|
|
3808
4008
|
child.on("error", (error) => {
|
|
3809
|
-
stderr
|
|
4009
|
+
stderr.take(Buffer.from(`the command could not start: ${error.message}\n`));
|
|
3810
4010
|
});
|
|
3811
4011
|
child.on("close", (code) => {
|
|
3812
4012
|
clearTimeout(timer);
|
|
3813
|
-
if (isTimedOut) stderr
|
|
4013
|
+
if (isTimedOut) stderr.take(Buffer.from(`this command took longer than ${timeoutMs / 1e3} seconds, so it was stopped\n`));
|
|
4014
|
+
const printed = stdout.toText();
|
|
4015
|
+
const said = stderr.toText() ?? "";
|
|
4016
|
+
const ended = isTimedOut ? 1 : code ?? 1;
|
|
4017
|
+
if (printed !== void 0) {
|
|
4018
|
+
resolve({
|
|
4019
|
+
stdout: printed,
|
|
4020
|
+
stderr: said,
|
|
4021
|
+
code: ended
|
|
4022
|
+
});
|
|
4023
|
+
return;
|
|
4024
|
+
}
|
|
3814
4025
|
resolve({
|
|
3815
|
-
stdout,
|
|
3816
|
-
stderr,
|
|
3817
|
-
code:
|
|
4026
|
+
stdout: "",
|
|
4027
|
+
stderr: ended === 0 ? `${said}this answer is ${stdout.bytes()} bytes that are not text, so it cannot be read here. Keep it with --output and a new file name, through ${WRITE_TOOL.name}\n` : said,
|
|
4028
|
+
code: ended || 1
|
|
3818
4029
|
});
|
|
3819
4030
|
});
|
|
3820
4031
|
child.stdin.on("error", () => void 0);
|
|
3821
4032
|
child.stdin.end(input ?? "");
|
|
3822
4033
|
});
|
|
3823
4034
|
}
|
|
4035
|
+
/** The most output kept from one command, well past what a result carries, so memory stays bounded. */
|
|
4036
|
+
const KEPT_BYTES = 4 * RESULT_CHARACTERS;
|
|
4037
|
+
/**
|
|
4038
|
+
* toCollector gathers what a command prints as bytes. Decoding each chunk on its own would
|
|
4039
|
+
* split a character that straddles two chunks, and would hide that a file is not text.
|
|
4040
|
+
*/
|
|
4041
|
+
function toCollector() {
|
|
4042
|
+
const chunks = [];
|
|
4043
|
+
let kept = 0;
|
|
4044
|
+
let seen = 0;
|
|
4045
|
+
return {
|
|
4046
|
+
take: (chunk) => {
|
|
4047
|
+
seen += chunk.length;
|
|
4048
|
+
if (kept < KEPT_BYTES) {
|
|
4049
|
+
chunks.push(chunk);
|
|
4050
|
+
kept += chunk.length;
|
|
4051
|
+
}
|
|
4052
|
+
},
|
|
4053
|
+
bytes: () => seen,
|
|
4054
|
+
toText: () => {
|
|
4055
|
+
let text;
|
|
4056
|
+
try {
|
|
4057
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks), { stream: true });
|
|
4058
|
+
} catch {
|
|
4059
|
+
return;
|
|
4060
|
+
}
|
|
4061
|
+
return seen > kept ? `${text}\n[${seen - kept} more bytes were not kept]` : text;
|
|
4062
|
+
}
|
|
4063
|
+
};
|
|
4064
|
+
}
|
|
3824
4065
|
/** serve answers requests on stdin until the client closes it. */
|
|
3825
4066
|
async function serve(commands, run = toCliRunner()) {
|
|
3826
4067
|
const lines = createInterface$1({ input: process.stdin });
|
|
@@ -6674,6 +6915,10 @@ async function toExitCode(globals, command, args, flags) {
|
|
|
6674
6915
|
return error instanceof ConfigFailure ? ExitCode.USAGE : ExitCode.ERROR;
|
|
6675
6916
|
}
|
|
6676
6917
|
}
|
|
6918
|
+
/** writeCrash answers an error nothing caught, in the form the caller reads. */
|
|
6919
|
+
function writeCrash(error, isJSON) {
|
|
6920
|
+
writeFailure(toErrorCode(error), error instanceof Error ? error.message : String(error), isJSON);
|
|
6921
|
+
}
|
|
6677
6922
|
/** toReported reports a defect, and tells a person at a terminal that it was reported. */
|
|
6678
6923
|
async function toReported(error, isJSON, resolved, level = "error") {
|
|
6679
6924
|
const invocation = toInvocation(commands, process.argv.slice(2));
|
|
@@ -6687,7 +6932,7 @@ async function toReported(error, isJSON, resolved, level = "error") {
|
|
|
6687
6932
|
if (id && !isJSON) process.stderr.write(`This looks like a problem in hardfin itself, and it has been reported to the Hardfin team. Reference: ${id}\n`);
|
|
6688
6933
|
}
|
|
6689
6934
|
//#endregion
|
|
6690
|
-
export { toCli, toParseExitCode, toReported, wasReportAttempted };
|
|
6935
|
+
export { toCli, toParseExitCode, toReported, wasReportAttempted, writeCrash };
|
|
6691
6936
|
|
|
6692
|
-
//# sourceMappingURL=program-
|
|
6693
|
-
//# debugId=
|
|
6937
|
+
//# sourceMappingURL=program-Bz-DwZDJ.js.map
|
|
6938
|
+
//# debugId=8bcae06e-d313-56ed-bc3e-49b505487fa3
|
|
@@ -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]="
|
|
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]="76640861-61e5-5004-ae78-dee93e00338f")}catch(e){}}();
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
//#region src/command/registry.ts
|
|
@@ -26,7 +26,7 @@ const GLOBAL_FLAGS = [
|
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
name: "api-key",
|
|
29
|
-
description: "An API key to call with,
|
|
29
|
+
description: "An API key to call with. Prefer HARDFIN_API_KEY, because a command line is visible to other programs",
|
|
30
30
|
valueName: "key",
|
|
31
31
|
schema: z.string()
|
|
32
32
|
},
|
|
@@ -118,5 +118,5 @@ function toFlag(flags, word) {
|
|
|
118
118
|
//#endregion
|
|
119
119
|
export { defineCommand as a, GLOBAL_FLAGS as i, toScrubbed as n, ExitCode as r, toInvocation as t };
|
|
120
120
|
|
|
121
|
-
//# sourceMappingURL=scrub-
|
|
122
|
-
//# debugId=
|
|
121
|
+
//# sourceMappingURL=scrub-BZ0egWxC.js.map
|
|
122
|
+
//# debugId=76640861-61e5-5004-ae78-dee93e00338f
|
|
@@ -1,7 +1,7 @@
|
|
|
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]="
|
|
4
|
-
import { n as toScrubbed } from "./scrub-
|
|
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]="12bc3b97-183e-5e0e-84a2-4353d0ade50b")}catch(e){}}();
|
|
4
|
+
import { n as toScrubbed } from "./scrub-BZ0egWxC.js";
|
|
5
5
|
import { dirname } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { request } from "node:http";
|
|
@@ -4385,5 +4385,5 @@ function toTags(delivery) {
|
|
|
4385
4385
|
//#endregion
|
|
4386
4386
|
export { send };
|
|
4387
4387
|
|
|
4388
|
-
//# sourceMappingURL=sentry-
|
|
4389
|
-
//# debugId=
|
|
4388
|
+
//# sourceMappingURL=sentry-e3Tum2kz.js.map
|
|
4389
|
+
//# debugId=12bc3b97-183e-5e0e-84a2-4353d0ade50b
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hardfin/cli",
|
|
3
|
-
"version": "0.1.1-dev.
|
|
3
|
+
"version": "0.1.1-dev.45",
|
|
4
4
|
"description": "Command line interface for the Hardfin API",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Hardfin, Inc.",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"templates"
|
|
30
30
|
],
|
|
31
31
|
"engines": {
|
|
32
|
-
"node": ">=20"
|
|
32
|
+
"node": ">=20.12"
|
|
33
33
|
},
|
|
34
34
|
"publishConfig": {
|
|
35
35
|
"access": "public"
|
|
@@ -17,7 +17,7 @@ flags, so check `--help` before relying on a flag this skill does not show.
|
|
|
17
17
|
hardfin --version || npm install -g @hardfin/cli
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
It needs Node 20 or later. If `npm install -g` is refused, run commands as
|
|
20
|
+
It needs Node 20.12 or later. If `npm install -g` is refused, run commands as
|
|
21
21
|
`npx -y @hardfin/cli <command>` instead.
|
|
22
22
|
|
|
23
23
|
## 2. Make sure it is signed in
|
|
@@ -28,11 +28,11 @@ hardfin status --offline --json
|
|
|
28
28
|
|
|
29
29
|
`credential.kind` is `none` when nobody is signed in. Then sign in one of three ways.
|
|
30
30
|
|
|
31
|
-
| Situation | Do this
|
|
32
|
-
| ---------------------------------------------------------- |
|
|
33
|
-
| You run on the person's own machine | Run `hardfin login` in the foreground
|
|
34
|
-
| You run in a cloud sandbox, away from the person's browser | Run `hardfin login --device`
|
|
35
|
-
| The person gave you an API key |
|
|
31
|
+
| Situation | Do this |
|
|
32
|
+
| ---------------------------------------------------------- | ---------------------------------------- |
|
|
33
|
+
| You run on the person's own machine | Run `hardfin login` in the foreground |
|
|
34
|
+
| You run in a cloud sandbox, away from the person's browser | Run `hardfin login --device` |
|
|
35
|
+
| The person gave you an API key | Set `HARDFIN_API_KEY` in the environment |
|
|
36
36
|
|
|
37
37
|
`hardfin login` writes one JSON line on stderr before it waits:
|
|
38
38
|
|
|
@@ -44,7 +44,9 @@ hardfin status --offline --json
|
|
|
44
44
|
It then waits up to `waitSeconds` (300) for the person to approve. Give the command that
|
|
45
45
|
long to finish. Do not run it in the background and move on.
|
|
46
46
|
|
|
47
|
-
Never write an API key into a file in the repository, and never echo it back.
|
|
47
|
+
Never write an API key into a file in the repository, and never echo it back. Never pass it
|
|
48
|
+
as `--api-key` either, because a command line is kept in transcripts and shown to other
|
|
49
|
+
programs on the machine.
|
|
48
50
|
|
|
49
51
|
## 3. Find the command
|
|
50
52
|
|
package/templates/AGENTS.md
CHANGED
|
@@ -4,7 +4,7 @@ This project works with Hardfin data through the Hardfin CLI, `hardfin`.
|
|
|
4
4
|
|
|
5
5
|
| Task | Command |
|
|
6
6
|
| ---------------------------------- | ---------------------------------------------------------------------------- |
|
|
7
|
-
| Install | `npm install -g @hardfin/cli`, with Node 20 or later
|
|
7
|
+
| Install | `npm install -g @hardfin/cli`, with Node 20.12 or later |
|
|
8
8
|
| Sign in | `hardfin login`, or `--device` away from a browser, or set `HARDFIN_API_KEY` |
|
|
9
9
|
| Check what it is connected to | `hardfin status` |
|
|
10
10
|
| Find a command | `hardfin --help`, then `hardfin <noun> --help` |
|