@hardfin/cli 0.1.1-dev.44 → 0.1.1-dev.46
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 +17 -8
- package/dist/cli.js +9 -6
- package/dist/completion-tree.json +1 -1
- package/dist/{program-CgT1Kr8L.js → program-Da7dZSnW.js} +178 -102
- package/dist/{scrub-BN3gyBpR.js → scrub-BZ0egWxC.js} +4 -4
- package/dist/{sentry-BM9Rvhhy.js → sentry-CC0H3uMB.js} +34 -19
- 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
|
|
@@ -183,7 +185,9 @@ hardfin logout # forget this machine, and ask Hardfin to revoke it
|
|
|
183
185
|
```
|
|
184
186
|
|
|
185
187
|
An API key is still the way to run unattended. `--api-key`, or `HARDFIN_API_KEY`, wins over
|
|
186
|
-
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.
|
|
187
191
|
|
|
188
192
|
An API key is held to two rules before it is sent:
|
|
189
193
|
|
|
@@ -202,7 +206,8 @@ how a local build is set up. Pass `--api-url` to send a key you set to that fold
|
|
|
202
206
|
2. Opens a loopback listener on `127.0.0.1`, over IPv4, on whatever port the machine hands out
|
|
203
207
|
3. Opens the browser to approve this CLI, with a PKCE challenge and a state value
|
|
204
208
|
4. Prints the URL, and waits at the keyboard as well as on the listener
|
|
205
|
-
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
|
|
206
211
|
6. Keeps the refresh token, and nothing else
|
|
207
212
|
|
|
208
213
|
While it waits, the terminal takes two things.
|
|
@@ -494,7 +499,8 @@ Only defects are reported. None of these are:
|
|
|
494
499
|
- a network failure, or a server that cannot be reached
|
|
495
500
|
- a mistake on the command line or in a settings file
|
|
496
501
|
- being signed out, or a sign in that was cancelled or refused
|
|
497
|
-
- 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
|
|
498
504
|
- anything from a run with no API key and no stored sign in, except a defect in `hardfin login`
|
|
499
505
|
|
|
500
506
|
The last rule is there because package scanners install the CLI and run it in sandboxes. They
|
|
@@ -505,8 +511,9 @@ A report carries the error, its stack trace, and what `hardfin status` would pri
|
|
|
505
511
|
moment. The command is named, and its flags are listed by name without their values.
|
|
506
512
|
|
|
507
513
|
A report never carries tokens, API keys, argument or flag values, request or response bodies,
|
|
508
|
-
people's or organizations' names, your hostname, or your home directory.
|
|
509
|
-
organization appear only as their Hardfin keys.
|
|
514
|
+
people's or organizations' names, an API key's label, your hostname, or your home directory.
|
|
515
|
+
A person and an organization appear only as their Hardfin keys and IDs. The report names the
|
|
516
|
+
few parts of an identity it carries, so a part the API adds later is left out.
|
|
510
517
|
|
|
511
518
|
| To | Set |
|
|
512
519
|
| ------------------------------------------------- | ------------------------------------------ |
|
|
@@ -532,9 +539,11 @@ settings, and the one nearest the top wins.
|
|
|
532
539
|
|
|
533
540
|
An exported variable beats `.env`, because the CLI leaves a variable that is already set alone.
|
|
534
541
|
|
|
535
|
-
An env file sets
|
|
536
|
-
|
|
537
|
-
|
|
542
|
+
An env file sets the four settings: `HARDFIN_API_URL`, `HARDFIN_API_KEY`, `HARDFIN_CLIENT_ID`
|
|
543
|
+
and `HARDFIN_ISSUER_URL`. It may also turn error reports off, with `DO_NOT_TRACK=1` or
|
|
544
|
+
`HARDFIN_TELEMETRY=0`. Anything else in it is ignored, so a `.env` in a folder you cloned
|
|
545
|
+
cannot set `NODE_OPTIONS` for the programs the CLI starts, or send error reports elsewhere
|
|
546
|
+
with `HARDFIN_SENTRY_DSN`. Export a variable yourself to set it.
|
|
538
547
|
|
|
539
548
|
### What a local build writes
|
|
540
549
|
|
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]="6c054d7b-a0a8-526c-90d7-5bbec53675dd")}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-Da7dZSnW.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-Da7dZSnW.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=6c054d7b-a0a8-526c-90d7-5bbec53675dd
|
|
@@ -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]="e0ad2183-1cb1-5831-b5d1-b96dcf795d93")}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. */
|
|
@@ -660,9 +770,23 @@ async function toMetadata(issuer, signal) {
|
|
|
660
770
|
const parsed = AuthorizationServerMetadata.safeParse(body);
|
|
661
771
|
if (!parsed.success) throw new DiscoveryFailure(`${url} does not describe an authorization server`);
|
|
662
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`);
|
|
663
775
|
keep(issuer, parsed.data);
|
|
664
776
|
return parsed.data;
|
|
665
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
|
+
}
|
|
666
790
|
/** toCachePath names where one server's document is kept, beside the credentials. */
|
|
667
791
|
function toCachePath(issuer) {
|
|
668
792
|
const name = createHash("sha256").update(issuer).digest("hex").slice(0, 16);
|
|
@@ -675,7 +799,7 @@ function toCached(issuer) {
|
|
|
675
799
|
const held = JSON.parse(readFileSync(path, "utf8"));
|
|
676
800
|
if ((held.readAt ?? 0) + CACHE_MS < Date.now()) return;
|
|
677
801
|
const parsed = AuthorizationServerMetadata.safeParse(held.document);
|
|
678
|
-
return parsed.success ? parsed.data : void 0;
|
|
802
|
+
return parsed.success && !toPlainEndpoint(parsed.data) ? parsed.data : void 0;
|
|
679
803
|
} catch {
|
|
680
804
|
return;
|
|
681
805
|
}
|
|
@@ -929,10 +1053,6 @@ function toHost$1(url) {
|
|
|
929
1053
|
return;
|
|
930
1054
|
}
|
|
931
1055
|
}
|
|
932
|
-
/** isLoopback reports whether a host is this machine. */
|
|
933
|
-
function isLoopback(hostname) {
|
|
934
|
-
return hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]" || hostname === "localhost";
|
|
935
|
-
}
|
|
936
1056
|
/**
|
|
937
1057
|
* toHeldCredential answers a credential without asking the server for one: the API key, or
|
|
938
1058
|
* an access token this process already holds. An error report uses it, because a refresh
|
|
@@ -1051,6 +1171,35 @@ function toStatusMessage(status) {
|
|
|
1051
1171
|
return `the request failed with status ${status}`;
|
|
1052
1172
|
}
|
|
1053
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
|
|
1054
1203
|
//#region src/report/classify.ts
|
|
1055
1204
|
/**
|
|
1056
1205
|
* The codes Node gives when the machine refuses something: permissions, a full or read-only
|
|
@@ -1077,8 +1226,9 @@ function isReportable(error) {
|
|
|
1077
1226
|
}
|
|
1078
1227
|
/** isMachineRefusal answers whether an error is the machine refusing a file or a folder. */
|
|
1079
1228
|
function isMachineRefusal(error) {
|
|
1080
|
-
const code = error instanceof Error ? error
|
|
1081
|
-
|
|
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));
|
|
1082
1232
|
}
|
|
1083
1233
|
//#endregion
|
|
1084
1234
|
//#region src/output/failure.ts
|
|
@@ -1170,7 +1320,7 @@ function toGuide(commands, version) {
|
|
|
1170
1320
|
"",
|
|
1171
1321
|
"## Before calling",
|
|
1172
1322
|
"",
|
|
1173
|
-
"- 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",
|
|
1174
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`",
|
|
1175
1325
|
"- Every command prints JSON on stdout and diagnostics on stderr",
|
|
1176
1326
|
"- Output is JSON whenever stdout is not a terminal, so no flag is needed when calling from code",
|
|
@@ -2213,14 +2363,6 @@ function toFiles(directory) {
|
|
|
2213
2363
|
return statSync(path).isDirectory() ? toFiles(path) : [path];
|
|
2214
2364
|
});
|
|
2215
2365
|
}
|
|
2216
|
-
/** toPackageDirectory finds this package's own folder, which holds the skills and templates it ships. */
|
|
2217
|
-
function toPackageDirectory(from = dirname(fileURLToPath(import.meta.url))) {
|
|
2218
|
-
for (let directory = from;; directory = dirname(directory)) {
|
|
2219
|
-
const manifest = join(directory, "package.json");
|
|
2220
|
-
if (existsSync(manifest) && JSON.parse(readFileSync(manifest, "utf8")).name === "@hardfin/cli") return directory;
|
|
2221
|
-
if (dirname(directory) === directory) throw new Error("the @hardfin/cli package folder could not be found, so its skills cannot be copied");
|
|
2222
|
-
}
|
|
2223
|
-
}
|
|
2224
2366
|
//#endregion
|
|
2225
2367
|
//#region src/command/types.generated.ts
|
|
2226
2368
|
/** API_TYPES is the TypeScript hardfin types prints. */
|
|
@@ -2681,8 +2823,8 @@ async function runDeviceLogin(input, metadata, scope) {
|
|
|
2681
2823
|
});
|
|
2682
2824
|
else process.stderr.write([
|
|
2683
2825
|
"",
|
|
2684
|
-
` Code ${device.userCode}`,
|
|
2685
|
-
` At ${device.verificationUri}`,
|
|
2826
|
+
` Code ${toPrintable(device.userCode)}`,
|
|
2827
|
+
` At ${toPrintable(device.verificationUri)}`,
|
|
2686
2828
|
"",
|
|
2687
2829
|
opened ? "Opened your browser there. Waiting for approval\n" : "Open that page on any machine, and enter the code. Waiting for approval\n"
|
|
2688
2830
|
].join("\n"));
|
|
@@ -3126,76 +3268,6 @@ function toReal(path) {
|
|
|
3126
3268
|
const parent = dirname(path);
|
|
3127
3269
|
return parent === path ? path : join(toReal(parent), basename(path));
|
|
3128
3270
|
}
|
|
3129
|
-
const OFF = /* @__PURE__ */ new Set([
|
|
3130
|
-
"0",
|
|
3131
|
-
"false",
|
|
3132
|
-
"off",
|
|
3133
|
-
"no"
|
|
3134
|
-
]);
|
|
3135
|
-
/**
|
|
3136
|
-
* toTelemetry decides whether defects are reported. A person's opt out wins over everything,
|
|
3137
|
-
* and a CLI run from a checkout reports only when its developer asks it to.
|
|
3138
|
-
*/
|
|
3139
|
-
function toTelemetry(env = process.env, isCheckout = isRunFromCheckout()) {
|
|
3140
|
-
const dsn = env["HARDFIN_SENTRY_DSN"] || "https://a4819e652e9fa63c0ab67e9febe44565@o1388102.ingest.us.sentry.io/4512176532684800";
|
|
3141
|
-
const base = {
|
|
3142
|
-
dsn,
|
|
3143
|
-
environment: isCheckout ? "local" : version$1.includes("-dev.") ? "preview" : "production",
|
|
3144
|
-
release: toRelease(version$1)
|
|
3145
|
-
};
|
|
3146
|
-
if (env["DO_NOT_TRACK"] && !OFF.has(env["DO_NOT_TRACK"].toLowerCase())) return {
|
|
3147
|
-
...base,
|
|
3148
|
-
enabled: false,
|
|
3149
|
-
from: "DO_NOT_TRACK"
|
|
3150
|
-
};
|
|
3151
|
-
const asked = env["HARDFIN_TELEMETRY"]?.toLowerCase();
|
|
3152
|
-
if (asked !== void 0 && OFF.has(asked)) return {
|
|
3153
|
-
...base,
|
|
3154
|
-
enabled: false,
|
|
3155
|
-
from: "HARDFIN_TELEMETRY"
|
|
3156
|
-
};
|
|
3157
|
-
if (!isDsn(dsn)) return {
|
|
3158
|
-
...base,
|
|
3159
|
-
enabled: false,
|
|
3160
|
-
from: "HARDFIN_SENTRY_DSN is not a valid DSN"
|
|
3161
|
-
};
|
|
3162
|
-
if (asked !== void 0) return {
|
|
3163
|
-
...base,
|
|
3164
|
-
enabled: true,
|
|
3165
|
-
from: "HARDFIN_TELEMETRY"
|
|
3166
|
-
};
|
|
3167
|
-
return isCheckout ? {
|
|
3168
|
-
...base,
|
|
3169
|
-
enabled: false,
|
|
3170
|
-
from: "run from a checkout"
|
|
3171
|
-
} : {
|
|
3172
|
-
...base,
|
|
3173
|
-
enabled: true,
|
|
3174
|
-
from: "default"
|
|
3175
|
-
};
|
|
3176
|
-
}
|
|
3177
|
-
/**
|
|
3178
|
-
* isRunFromCheckout answers whether this CLI runs from a clone of its repository, as npm link
|
|
3179
|
-
* and node dist/cli.js do. The published package holds dist and nothing beside it.
|
|
3180
|
-
*/
|
|
3181
|
-
function isRunFromCheckout() {
|
|
3182
|
-
return existsSync(fileURLToPath(new URL("../src", import.meta.url)));
|
|
3183
|
-
}
|
|
3184
|
-
/** isDsn answers whether a DSN can be read as the URL of a Sentry project. */
|
|
3185
|
-
function isDsn(dsn) {
|
|
3186
|
-
try {
|
|
3187
|
-
return new URL(dsn).host !== "";
|
|
3188
|
-
} catch {
|
|
3189
|
-
return false;
|
|
3190
|
-
}
|
|
3191
|
-
}
|
|
3192
|
-
/**
|
|
3193
|
-
* toRelease names a version as Sentry knows it. Sentry refuses a slash in a release name, so
|
|
3194
|
-
* the scoped package name @hardfin/cli cannot be used, and hardfin-cli stands in for it.
|
|
3195
|
-
*/
|
|
3196
|
-
function toRelease(of) {
|
|
3197
|
-
return `hardfin-cli@${of}`;
|
|
3198
|
-
}
|
|
3199
3271
|
//#endregion
|
|
3200
3272
|
//#region src/system/host.ts
|
|
3201
3273
|
const BYTES_PER_GB = 1024 ** 3;
|
|
@@ -3502,7 +3574,7 @@ async function report(error, occasion, resolved) {
|
|
|
3502
3574
|
try {
|
|
3503
3575
|
const status = settings ? await toStatus(settings) : void 0;
|
|
3504
3576
|
const secrets = [settings?.settings.apiKey, settings ? toCredential(settings.settings.issuerUrl)?.refreshToken : void 0];
|
|
3505
|
-
return await (await import("./sentry-
|
|
3577
|
+
return await (await import("./sentry-CC0H3uMB.js")).send({
|
|
3506
3578
|
error,
|
|
3507
3579
|
occasion,
|
|
3508
3580
|
telemetry,
|
|
@@ -6843,6 +6915,10 @@ async function toExitCode(globals, command, args, flags) {
|
|
|
6843
6915
|
return error instanceof ConfigFailure ? ExitCode.USAGE : ExitCode.ERROR;
|
|
6844
6916
|
}
|
|
6845
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
|
+
}
|
|
6846
6922
|
/** toReported reports a defect, and tells a person at a terminal that it was reported. */
|
|
6847
6923
|
async function toReported(error, isJSON, resolved, level = "error") {
|
|
6848
6924
|
const invocation = toInvocation(commands, process.argv.slice(2));
|
|
@@ -6856,7 +6932,7 @@ async function toReported(error, isJSON, resolved, level = "error") {
|
|
|
6856
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`);
|
|
6857
6933
|
}
|
|
6858
6934
|
//#endregion
|
|
6859
|
-
export { toCli, toParseExitCode, toReported, wasReportAttempted };
|
|
6935
|
+
export { toCli, toParseExitCode, toReported, wasReportAttempted, writeCrash };
|
|
6860
6936
|
|
|
6861
|
-
//# sourceMappingURL=program-
|
|
6862
|
-
//# debugId=
|
|
6937
|
+
//# sourceMappingURL=program-Da7dZSnW.js.map
|
|
6938
|
+
//# debugId=e0ad2183-1cb1-5831-b5d1-b96dcf795d93
|
|
@@ -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]="8beb7849-4488-5315-88c8-ae70cca2939f")}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";
|
|
@@ -4331,33 +4331,48 @@ function toIdentity(status) {
|
|
|
4331
4331
|
return (status?.["api"])?.identity;
|
|
4332
4332
|
}
|
|
4333
4333
|
/**
|
|
4334
|
-
*
|
|
4335
|
-
*
|
|
4334
|
+
* The parts of an identity a report carries. Each identifies the credential, the person or
|
|
4335
|
+
* the organization without saying who they are. Anything the API adds later is left out
|
|
4336
|
+
* until someone decides it belongs here.
|
|
4337
|
+
*/
|
|
4338
|
+
const IDENTITY_FIELDS = [
|
|
4339
|
+
"type",
|
|
4340
|
+
"identifier",
|
|
4341
|
+
"scopes",
|
|
4342
|
+
"createdAt",
|
|
4343
|
+
"expiresAt",
|
|
4344
|
+
"lastRegeneratedAt",
|
|
4345
|
+
"unavailable"
|
|
4346
|
+
];
|
|
4347
|
+
/** The parts of a person or an organization a report carries, which are how Hardfin finds them. */
|
|
4348
|
+
const RECORD_FIELDS = ["id", "key"];
|
|
4349
|
+
/**
|
|
4350
|
+
* toPrivate keeps only what identifies who a credential is from the status report. A name
|
|
4351
|
+
* is left out, and so is a key's label, which is whatever a person typed when they made it.
|
|
4336
4352
|
*/
|
|
4337
4353
|
function toPrivate(status) {
|
|
4338
|
-
const identity = toIdentity(status);
|
|
4339
|
-
if (!identity) return status;
|
|
4340
4354
|
const api = status["api"];
|
|
4341
|
-
|
|
4355
|
+
if (!isRecord(api) || !isRecord(api["identity"])) return status;
|
|
4356
|
+
const identity = api["identity"];
|
|
4342
4357
|
return {
|
|
4343
4358
|
...status,
|
|
4344
4359
|
api: {
|
|
4345
4360
|
...api,
|
|
4346
4361
|
identity: {
|
|
4347
|
-
...
|
|
4348
|
-
organization: organization ?
|
|
4349
|
-
|
|
4350
|
-
name: void 0
|
|
4351
|
-
} : void 0,
|
|
4352
|
-
person: person ? {
|
|
4353
|
-
...person,
|
|
4354
|
-
firstName: void 0,
|
|
4355
|
-
lastName: void 0
|
|
4356
|
-
} : void 0
|
|
4362
|
+
...toPicked(identity, IDENTITY_FIELDS),
|
|
4363
|
+
organization: isRecord(identity["organization"]) ? toPicked(identity["organization"], RECORD_FIELDS) : void 0,
|
|
4364
|
+
person: isRecord(identity["person"]) ? toPicked(identity["person"], RECORD_FIELDS) : void 0
|
|
4357
4365
|
}
|
|
4358
4366
|
}
|
|
4359
4367
|
};
|
|
4360
4368
|
}
|
|
4369
|
+
function isRecord(value) {
|
|
4370
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4371
|
+
}
|
|
4372
|
+
/** toPicked copies the named fields of a record, and none of the others. */
|
|
4373
|
+
function toPicked(record, fields) {
|
|
4374
|
+
return Object.fromEntries(fields.filter((field) => field in record).map((field) => [field, record[field]]));
|
|
4375
|
+
}
|
|
4361
4376
|
/** toTags picks the few values worth searching by, each with few distinct values. */
|
|
4362
4377
|
function toTags(delivery) {
|
|
4363
4378
|
const { occasion, status } = delivery;
|
|
@@ -4385,5 +4400,5 @@ function toTags(delivery) {
|
|
|
4385
4400
|
//#endregion
|
|
4386
4401
|
export { send };
|
|
4387
4402
|
|
|
4388
|
-
//# sourceMappingURL=sentry-
|
|
4389
|
-
//# debugId=
|
|
4403
|
+
//# sourceMappingURL=sentry-CC0H3uMB.js.map
|
|
4404
|
+
//# debugId=8beb7849-4488-5315-88c8-ae70cca2939f
|
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.46",
|
|
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` |
|