@hardfin/cli 0.1.1-dev.68 → 0.1.1-dev.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,6 +24,57 @@ Previews of unreleased work are published from the `dev` branch.
24
24
  npm install -g @hardfin/cli@dev
25
25
  ```
26
26
 
27
+ ### Staying up to date
28
+
29
+ Hardfin changes its API from time to time in ways an older CLI cannot follow. The CLI checks
30
+ npm for a newer version, and `hardfin update` installs it.
31
+
32
+ ```sh
33
+ hardfin update --check # say whether a newer version is published
34
+ hardfin update # install it, asking first
35
+ hardfin update --yes # install it without asking, as an agent does
36
+ ```
37
+
38
+ The check asks npm's registry once a day, beside the command rather than before it. It asks
39
+ again at once when a command fails in a way a newer CLI may fix:
40
+
41
+ - The API refused the version this CLI speaks, which is `cli_outdated`
42
+ - The API refused the request, which is `bad_request` or `invalid_request`
43
+ - The CLI did not know a command, flag or body field, which is `unknown_command`, `invalid_flag` or `invalid_input`
44
+ - An answer carried the API's `API_VERSION_OUTDATED` notice
45
+
46
+ A newer version is announced on stderr after the command's own output. A person reads a
47
+ sentence. Anything else reads one JSON line, which carries `event` where a failure carries `code`.
48
+
49
+ | `event` | Fields | Means |
50
+ | ---------------------- | ---------------------------------------- | ------------------------------------------------------- |
51
+ | `update_available` | `version`, `latest`, `reason`, `command` | A newer CLI is published. `command` installs it |
52
+ | `api_version_outdated` | `apiVersion`, `latestVersion`, `command` | The API has moved on, and no newer CLI is published yet |
53
+
54
+ `reason` is `reminder` for the daily notice, which is printed once a day for each version.
55
+ It is `failure` or `api_version_outdated` when the notice follows a failure or the API's
56
+ notice, and those are printed every time. Set `HARDFIN_NO_UPDATE_CHECK=1` to turn the check
57
+ off. A run with `CI` set never checks, and neither does `hardfin mcp`.
58
+
59
+ `hardfin update` installs only where it owns the install. It never runs `sudo`.
60
+
61
+ | Installed with | `hardfin update` |
62
+ | ---------------------------------------------------- | -------------------------------------------------------------------- |
63
+ | `npm install -g`, in a folder this account can write | Runs `npm install -g` with the same prefix, then refreshes the skill |
64
+ | `npm install -g`, in a folder another account owns | Prints `npm install -g @hardfin/cli@latest`, and fails |
65
+ | `npx` | Prints `npx -y @hardfin/cli@latest`, and fails |
66
+ | Volta, pnpm, Yarn or Bun | Prints that tool's install command, and fails |
67
+ | A project's `package.json` | Prints `npm install @hardfin/cli@latest` for that project, and fails |
68
+ | A clone of this repository | Prints the git and build commands, and fails |
69
+
70
+ A failure here has the code `update_unavailable`, and its message ends with the command to
71
+ run. A preview updates to the newer of the next preview and the next release.
72
+
73
+ After installing, `hardfin update` runs `hardfin init --skills-only` with the new version when
74
+ the working directory holds the skill `hardfin init` wrote. That brings the skill up to date
75
+ and leaves `AGENTS.md` and the MCP configs alone. Run `hardfin init --skills-only` in any
76
+ other project you set up.
77
+
27
78
  ## The command surface
28
79
 
29
80
  Most commands are generated from the published API document rather than written by hand.
@@ -148,9 +199,10 @@ A field the API does not take null for is sent the word as text. The same holds
148
199
  hardfin asset accounting update ast_4f9xk2mq7plr8stz --unset useful-life
149
200
  ```
150
201
 
151
- Three things fail locally with exit code 2, before any request is made: a value outside an
152
- enum, a required field that is missing, and `--unset` on a field the API will not take null
153
- for.
202
+ Four things fail locally with exit code 2, before any request is made: a value outside an
203
+ enum, a required field that is missing, a list element missing a field every element needs,
204
+ and `--unset` on a field the API will not take null for. `--help` starts a required flag's
205
+ description with `Required.`, and names the fields every element of a repeated flag needs.
154
206
 
155
207
  ### When a command fails
156
208
 
@@ -169,7 +221,7 @@ otherwise have to read. Pipe a result through `jq .` to read it indented.
169
221
  | `requestId` | The API's request ID, or null when the request never reached it |
170
222
  | `status` | The HTTP status, or null for a failure caught before any request is made |
171
223
 
172
- Four codes cover failures that are easy to mistake for something else.
224
+ Five codes cover failures that are easy to mistake for something else.
173
225
 
174
226
  | `code` | Happens when |
175
227
  | --------------------- | ---------------------------------------------------------------------------------------------------- |
@@ -177,6 +229,7 @@ Four codes cover failures that are easy to mistake for something else.
177
229
  | `unexpected_response` | A 200 whose body is not the API's JSON, such as a proxy's page. It is never an empty result |
178
230
  | `timed_out` | The server did not begin to answer within 60 seconds, or 300 for an upload |
179
231
  | `file_error` | This machine refused a path the command was given |
232
+ | `cli_outdated` | The API no longer serves the version of the endpoint this CLI speaks. Only `hardfin update` fixes it |
180
233
 
181
234
  A change that timed out may still have been made, and the message says so. Read the record
182
235
  before sending the change again.
@@ -225,16 +278,17 @@ hardfin agent-guide --json customer create | jq -r '.commands[0].flags[] | selec
225
278
  The JSON holds `cli`, `apiVersion`, `exitCodes`, `errorCodes`, `globalFlags` and `commands`.
226
279
  Each command names its `path`, its `endpoint`, whether it `runs`, and its flags.
227
280
 
228
- | A flag's | Holds |
229
- | ------------ | --------------------------------------------------------------------------- |
230
- | `required` | Present when the command fails without it |
231
- | `schema` | The `type`, and the `enum`, `format`, `minimum` or `maximum` that narrow it |
232
- | `sends` | The query parameter, or the path of the body field, the value goes to |
233
- | `clearable` | Present when `--unset` can clear the field |
234
- | `repeatable` | Present when the flag is given once per value |
235
- | `negatable` | Present when a boolean also takes a `--no-` form |
236
- | `element` | The fields one value carries as `key=value`, for an array of objects |
237
- | `default` | The value used when the flag is not given |
281
+ | A flag's | Holds |
282
+ | ----------------- | --------------------------------------------------------------------------- |
283
+ | `required` | Present when the command fails without it |
284
+ | `schema` | The `type`, and the `enum`, `format`, `minimum` or `maximum` that narrow it |
285
+ | `sends` | The query parameter, or the path of the body field, the value goes to |
286
+ | `clearable` | Present when `--unset` can clear the field |
287
+ | `repeatable` | Present when the flag is given once per value |
288
+ | `negatable` | Present when a boolean also takes a `--no-` form |
289
+ | `element` | The fields one value carries as `key=value`, for an array of objects |
290
+ | `elementRequired` | The fields of `element` every value must carry |
291
+ | `default` | The value used when the flag is not given |
238
292
 
239
293
  A part that is absent means no. The description is read from the same declarations the parser
240
294
  enforces, so it cannot say one thing while the command does another.
@@ -0,0 +1,500 @@
1
+ #!/usr/bin/env node
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]="9d637f5e-0cda-5582-80ff-432edc41bf8c")}catch(e){}}();
4
+ import { createRequire } from "node:module";
5
+ import { z } from "zod";
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { dirname, join, resolve, sep } from "node:path";
8
+ import { parseEnv } from "node:util";
9
+ import { fileURLToPath } from "node:url";
10
+ import { homedir } from "node:os";
11
+ //#region src/report/expected.ts
12
+ /**
13
+ * ExpectedFailure is a failure the CLI anticipated: the person's input, their network, their
14
+ * machine, or an answer from the API. It is shown to the person and never reported, and every
15
+ * error that does not extend it is reported as a defect.
16
+ */
17
+ var ExpectedFailure = class extends Error {};
18
+ /** LockHeld is another hardfin command holding the credential lock for too long. */
19
+ var LockHeld = class extends ExpectedFailure {
20
+ constructor() {
21
+ super("another hardfin command is holding the credential lock");
22
+ this.name = "LockHeld";
23
+ }
24
+ };
25
+ /**
26
+ * Interrupted is a person stopping the CLI with Ctrl-C. It is something they chose to do,
27
+ * not a failure, so the CLI stops quietly with the exit code a shell gives an interrupt.
28
+ */
29
+ var Interrupted = class extends ExpectedFailure {
30
+ constructor() {
31
+ super("interrupted");
32
+ this.name = "Interrupted";
33
+ }
34
+ };
35
+ /** TimedOut is something the CLI waited on for longer than it allows, such as the browser. */
36
+ var TimedOut = class extends ExpectedFailure {
37
+ constructor(message) {
38
+ super(message);
39
+ this.name = "TimedOut";
40
+ }
41
+ };
42
+ /** version is what this build reports, read from the published package manifest. */
43
+ const version$1 = createRequire(import.meta.url)("../package.json").version;
44
+ /** The values that mean no, for a variable that turns something on or off. */
45
+ const OFF = /* @__PURE__ */ new Set([
46
+ "0",
47
+ "false",
48
+ "off",
49
+ "no"
50
+ ]);
51
+ /**
52
+ * toTelemetry decides whether defects are reported. A person's opt out wins over everything,
53
+ * and a CLI run from a checkout reports only when its developer asks it to.
54
+ */
55
+ function toTelemetry(env = process.env, isCheckout = isRunFromCheckout()) {
56
+ const dsn = env["HARDFIN_SENTRY_DSN"] || "https://a4819e652e9fa63c0ab67e9febe44565@o1388102.ingest.us.sentry.io/4512176532684800";
57
+ const base = {
58
+ dsn,
59
+ environment: isCheckout ? "local" : version$1.includes("-dev.") ? "preview" : "production",
60
+ release: toRelease(version$1)
61
+ };
62
+ if (env["DO_NOT_TRACK"] && !OFF.has(env["DO_NOT_TRACK"].toLowerCase())) return {
63
+ ...base,
64
+ enabled: false,
65
+ from: "DO_NOT_TRACK"
66
+ };
67
+ const asked = env["HARDFIN_TELEMETRY"]?.toLowerCase();
68
+ if (asked !== void 0 && OFF.has(asked)) return {
69
+ ...base,
70
+ enabled: false,
71
+ from: "HARDFIN_TELEMETRY"
72
+ };
73
+ if (!isDsn(dsn)) return {
74
+ ...base,
75
+ enabled: false,
76
+ from: "HARDFIN_SENTRY_DSN is not a valid DSN"
77
+ };
78
+ if (asked !== void 0) return {
79
+ ...base,
80
+ enabled: true,
81
+ from: "HARDFIN_TELEMETRY"
82
+ };
83
+ return isCheckout ? {
84
+ ...base,
85
+ enabled: false,
86
+ from: "run from a checkout"
87
+ } : {
88
+ ...base,
89
+ enabled: true,
90
+ from: "default"
91
+ };
92
+ }
93
+ /**
94
+ * isRunFromCheckout answers whether this CLI runs from a clone of its repository, as npm link
95
+ * and node dist/cli.js do. The published package holds dist and nothing beside it.
96
+ */
97
+ function isRunFromCheckout() {
98
+ return existsSync(fileURLToPath(new URL("../src", import.meta.url)));
99
+ }
100
+ /** isDsn answers whether a DSN can be read as the URL of a Sentry project. */
101
+ function isDsn(dsn) {
102
+ try {
103
+ return new URL(dsn).host !== "";
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
108
+ /**
109
+ * toRelease names a version as Sentry knows it. Sentry refuses a slash in a release name, so
110
+ * the scoped package name @hardfin/cli cannot be used, and hardfin-cli stands in for it.
111
+ */
112
+ function toRelease(of) {
113
+ return `hardfin-cli@${of}`;
114
+ }
115
+ //#endregion
116
+ //#region src/config/settings.ts
117
+ /** The API version this build was written against, sent on every request. */
118
+ const API_VERSION = "2026-10-21";
119
+ /** The file a local build reads its overrides from, in the working directory. */
120
+ const CONFIG_FILE = "config.local.json";
121
+ const DEFAULT_API_URL = "https://api.hardfin.com/v2";
122
+ /**
123
+ * The client Hardfin seeded for this CLI. The key is generated from a fixed identifier, so
124
+ * it names the same first-party client in every environment.
125
+ */
126
+ const DEFAULT_CLIENT_ID = "oacl_10e2etq297nhjfhb";
127
+ const DEFAULT_ENV_FILE = ".env";
128
+ const FileSettings = z.strictObject({
129
+ apiUrl: z.string().optional(),
130
+ apiKey: z.string().optional(),
131
+ clientId: z.string().optional(),
132
+ issuerUrl: z.string().optional()
133
+ });
134
+ /** ConfigFailure is a config file that cannot be read or does not match the schema. */
135
+ var ConfigFailure = class extends ExpectedFailure {
136
+ constructor(message) {
137
+ super(message);
138
+ this.name = "ConfigFailure";
139
+ }
140
+ };
141
+ /** toSettings resolves what this invocation talks to, and where each value came from. */
142
+ function toSettings(flags = {}, directory = process.cwd()) {
143
+ const fromEnvFile = loadEnvFile(directory);
144
+ const file = toFileSettings(directory);
145
+ const sources = {};
146
+ const pick = (key, flag, variable, fromFile, fallback) => {
147
+ const [value, source] = toLayer(flag, process.env[variable], fromEnvFile.has(variable), fromFile, fallback);
148
+ sources[key] = source;
149
+ return value;
150
+ };
151
+ const apiUrl = toTrimmedUrl(pick("apiUrl", flags.apiUrl, "HARDFIN_API_URL", file.apiUrl, DEFAULT_API_URL) ?? DEFAULT_API_URL);
152
+ return {
153
+ settings: {
154
+ apiUrl,
155
+ apiKey: pick("apiKey", flags.apiKey, "HARDFIN_API_KEY", file.apiKey),
156
+ clientId: pick("clientId", flags.clientId, "HARDFIN_CLIENT_ID", file.clientId, DEFAULT_CLIENT_ID) ?? DEFAULT_CLIENT_ID,
157
+ issuerUrl: pick("issuerUrl", flags.issuerUrl, "HARDFIN_ISSUER_URL", file.issuerUrl, toOrigin(apiUrl)) ?? ""
158
+ },
159
+ sources
160
+ };
161
+ }
162
+ function toLayer(flag, environment, isFromEnvFile, file, fallback) {
163
+ if (flag) return [flag, "flag"];
164
+ if (environment) return [environment, isFromEnvFile ? "env file" : "environment"];
165
+ if (file) return [file, "config file"];
166
+ return [fallback, "default"];
167
+ }
168
+ /** toFileSettings reads the local override file, which a local build is expected to have. */
169
+ function toFileSettings(directory) {
170
+ const path = resolve(directory, CONFIG_FILE);
171
+ if (!existsSync(path)) return {};
172
+ let parsed;
173
+ try {
174
+ parsed = JSON.parse(readFileSync(path, "utf8"));
175
+ } catch {
176
+ throw new ConfigFailure(`${CONFIG_FILE} does not hold JSON`);
177
+ }
178
+ const result = FileSettings.safeParse(parsed);
179
+ if (!result.success) {
180
+ const issue = result.error.issues[0];
181
+ throw new ConfigFailure(`${CONFIG_FILE} is not valid: ${issue?.path.join(".") || "root"} ${issue?.message ?? ""}`.trim());
182
+ }
183
+ return result.data;
184
+ }
185
+ /** The variables an env file may set, which are the settings and nothing else the CLI reads. */
186
+ const SETTING_VARIABLES = /* @__PURE__ */ new Set([
187
+ "HARDFIN_API_URL",
188
+ "HARDFIN_API_KEY",
189
+ "HARDFIN_CLIENT_ID",
190
+ "HARDFIN_ISSUER_URL"
191
+ ]);
192
+ /**
193
+ * isTakenFromFile answers whether an env file may set a variable. A file in a folder supplies
194
+ * the settings, and may turn error reports off. Where reports go, where a sign in is kept,
195
+ * and anything a started program reads are for the person to set themselves.
196
+ */
197
+ function isTakenFromFile(name, value) {
198
+ if (SETTING_VARIABLES.has(name)) return true;
199
+ if (name === "DO_NOT_TRACK") return !OFF.has(value.toLowerCase());
200
+ return name === "HARDFIN_TELEMETRY" && OFF.has(value.toLowerCase());
201
+ }
202
+ /**
203
+ * loadEnvFile reads a .env beside the command, so a local build needs no exports, and
204
+ * answers which variables it supplied. An exported variable is left alone, so a shell
205
+ * export still wins over the file.
206
+ */
207
+ function loadEnvFile(directory) {
208
+ const path = process.env["HARDFIN_ENV_FILE"] ?? resolve(directory, DEFAULT_ENV_FILE);
209
+ if (!existsSync(path)) return /* @__PURE__ */ new Set();
210
+ let held;
211
+ try {
212
+ held = parseEnv(readFileSync(path, "utf8"));
213
+ } catch {
214
+ throw new ConfigFailure(`${path} cannot be read as an env file`);
215
+ }
216
+ const supplied = /* @__PURE__ */ new Set();
217
+ for (const [name, value] of Object.entries(held)) {
218
+ if (value === void 0 || process.env[name] !== void 0 || !isTakenFromFile(name, value)) continue;
219
+ process.env[name] = value;
220
+ supplied.add(name);
221
+ }
222
+ return supplied;
223
+ }
224
+ function toTrimmedUrl(url) {
225
+ return url.replace(/\/+$/, "");
226
+ }
227
+ /** toOrigin drops the API's path, because the authorization server sits at the host root. */
228
+ function toOrigin(apiUrl) {
229
+ try {
230
+ return new URL(apiUrl).origin;
231
+ } catch {
232
+ return apiUrl;
233
+ }
234
+ }
235
+ //#endregion
236
+ //#region src/output/writer.ts
237
+ /**
238
+ * toText reads a value a caller supplied, which arrives typed as unknown. An object would
239
+ * otherwise reach a request as the text "[object Object]".
240
+ */
241
+ function toText(value) {
242
+ if (typeof value === "string") return value;
243
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
244
+ if (value === void 0 || value === null) return "";
245
+ return JSON.stringify(value);
246
+ }
247
+ /** Every control character but a newline and a tab, which are the two a line of text needs. */
248
+ const CONTROL = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
249
+ /**
250
+ * toPrintable makes text safe to show at a terminal. A name or a message from the API can
251
+ * hold an escape sequence, which a terminal would act on. Each such character is written
252
+ * as its code, so the text reads as what it holds.
253
+ */
254
+ function toPrintable(text) {
255
+ return text.replace(CONTROL, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
256
+ }
257
+ /**
258
+ * toJSON writes a value for whoever is reading the stream. A person at a terminal gets it
259
+ * indented. A program gets it on one line, because the indentation of a page of records is
260
+ * close to half of what an agent would otherwise have to read.
261
+ */
262
+ function toJSON(value, stream) {
263
+ return stream.isTTY ? JSON.stringify(value, null, 2) : JSON.stringify(value);
264
+ }
265
+ /** writeData prints a command's result on stdout. */
266
+ function writeData(value) {
267
+ if (typeof value === "string") {
268
+ const text = toPrintable(value);
269
+ process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
270
+ return;
271
+ }
272
+ process.stdout.write(`${toJSON(value ?? null, process.stdout)}\n`);
273
+ }
274
+ /** The code of the last failure written, which decides whether the CLI checks for an update. */
275
+ let lastFailureCode;
276
+ /** toLastFailureCode answers the code of the last failure this run wrote. */
277
+ function toLastFailureCode() {
278
+ return lastFailureCode;
279
+ }
280
+ /** writeFailure prints why a command failed on stderr, with a code only a machine reading JSON sees. */
281
+ function writeFailure(code, message, isJSON, errors, requestId, status) {
282
+ lastFailureCode = code;
283
+ if (!isJSON) {
284
+ process.stderr.write(`error: ${toPrintable(message)}${status === void 0 ? "" : ` (HTTP ${status})`}\n`);
285
+ for (const entry of errors?.slice(1) ?? []) process.stderr.write(` ${toPrintable(entry.error)} (${entry.statusCode})\n`);
286
+ if (requestId) process.stderr.write(` request ${toPrintable(requestId)}\n`);
287
+ return;
288
+ }
289
+ const payload = {
290
+ code,
291
+ message,
292
+ errors: errors ?? [{
293
+ error: message,
294
+ statusCode: 0
295
+ }],
296
+ requestId: requestId ?? null,
297
+ status: status ?? null
298
+ };
299
+ process.stderr.write(`${toJSON(payload, process.stderr)}\n`);
300
+ }
301
+ /** isJSONOutput decides whether this invocation prints machine-readable output. */
302
+ function isJSONOutput(flags) {
303
+ if (flags["json"] === true) return true;
304
+ return !process.stdout.isTTY;
305
+ }
306
+ //#endregion
307
+ //#region src/system/state.ts
308
+ /** toStateDirectory names the folder the CLI keeps what it remembers between runs in. */
309
+ function toStateDirectory() {
310
+ const base = process.env["XDG_STATE_HOME"] ?? join(homedir(), ".local", "state");
311
+ return join(base, "hardfin");
312
+ }
313
+ /** The newest API version any answer in this run asked the CLI to move to. */
314
+ let outdatedTo;
315
+ /** noteNotices keeps what an answer's notices say about the version this CLI speaks. */
316
+ function noteNotices(notices) {
317
+ if (!Array.isArray(notices)) return;
318
+ for (const notice of notices) {
319
+ const { code, latestVersion } = typeof notice === "object" && notice !== null ? notice : {};
320
+ if (code !== "API_VERSION_OUTDATED" || typeof latestVersion !== "string") continue;
321
+ if (outdatedTo === void 0 || latestVersion > outdatedTo) outdatedTo = latestVersion;
322
+ }
323
+ }
324
+ /** toOutdatedTo answers the API version an answer said to move to, or nothing when none did. */
325
+ function toOutdatedTo() {
326
+ return outdatedTo;
327
+ }
328
+ //#endregion
329
+ //#region src/system/package.ts
330
+ /** The folders this package ships, beside its manifest. */
331
+ const SHIPPED = [
332
+ "dist",
333
+ "skills",
334
+ "templates"
335
+ ];
336
+ /** toPackageDirectory finds this package's own folder, which holds the skills and templates it ships. */
337
+ function toPackageDirectory(from = dirname(fileURLToPath(import.meta.url))) {
338
+ for (let directory = from;; directory = dirname(directory)) {
339
+ const manifest = join(directory, "package.json");
340
+ if (existsSync(manifest) && JSON.parse(readFileSync(manifest, "utf8")).name === "@hardfin/cli") return directory;
341
+ if (dirname(directory) === directory) throw new Error("the @hardfin/cli package folder could not be found, so its skills cannot be copied");
342
+ }
343
+ }
344
+ /**
345
+ * isShipped answers whether a path is one of the files this package installs. A file of the
346
+ * CLI's own that cannot be read is a broken install, not the person's machine refusing them.
347
+ */
348
+ function isShipped(path, packageDirectory) {
349
+ let directory;
350
+ try {
351
+ directory = packageDirectory ?? toPackageDirectory();
352
+ } catch {
353
+ return false;
354
+ }
355
+ return path === join(directory, "package.json") || SHIPPED.some((folder) => path === join(directory, folder) || path.startsWith(join(directory, folder) + sep));
356
+ }
357
+ //#endregion
358
+ //#region src/update/check.ts
359
+ /** The package the CLI is published as, which an update installs. */
360
+ const PACKAGE = "@hardfin/cli";
361
+ /** How long a registry request may take, which is short because no command waits on it for long. */
362
+ const REGISTRY_TIMEOUT_MS = 3e3;
363
+ /** A version as npm publishes this package, a release or a dev preview. */
364
+ const VERSION = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
365
+ /**
366
+ * toUpdateCheckRefusal says why the CLI should not ask the registry, or nothing when it may.
367
+ * A person turns it off with HARDFIN_NO_UPDATE_CHECK, and a CI run never asks.
368
+ */
369
+ function toUpdateCheckRefusal(env = process.env) {
370
+ const asked = env["HARDFIN_NO_UPDATE_CHECK"];
371
+ if (asked !== void 0 && asked !== "" && !OFF.has(asked.toLowerCase())) return "HARDFIN_NO_UPDATE_CHECK is set";
372
+ if (env["CI"] && !OFF.has(env["CI"].toLowerCase())) return "CI is set";
373
+ }
374
+ /**
375
+ * toCheck answers whether a newer version is published. It reads the cache while it is less
376
+ * than a day old, unless isFresh asks the registry regardless.
377
+ */
378
+ async function toCheck(options = {}) {
379
+ const cache = toCache();
380
+ const isCurrent = cache !== void 0 && Date.now() - Date.parse(cache.checkedAt) < 864e5;
381
+ if (cache && (options.isCachedOnly || isCurrent && !options.isFresh)) return toUpdateCheck(cache.tags, cache.checkedAt, true);
382
+ if (options.isCachedOnly) return;
383
+ const tags = await toRegistryTags(options.signal);
384
+ if (!tags) return;
385
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
386
+ writeCache({
387
+ ...cache,
388
+ checkedAt,
389
+ tags
390
+ });
391
+ return toUpdateCheck(tags, checkedAt, false);
392
+ }
393
+ /** toUpdateCheck compares the installed version with the newest one on its channel. */
394
+ function toUpdateCheck(tags, checkedAt, isCached, version = version$1) {
395
+ const tag = toChannel(version);
396
+ const latest = (tag === "dev" ? [tags.dev, tags.latest] : [tags.latest]).filter((entry) => entry !== void 0).reduce((newest, entry) => newest === void 0 || toCompared(entry, newest) > 0 ? entry : newest, void 0);
397
+ if (latest === void 0 || !VERSION.test(version)) return;
398
+ return {
399
+ version,
400
+ latest,
401
+ tag,
402
+ isOutdated: toCompared(latest, version) > 0,
403
+ checkedAt,
404
+ isCached
405
+ };
406
+ }
407
+ /** toChannel names the npm tag a version was published under. */
408
+ function toChannel(version) {
409
+ return /-dev\./.test(version) ? "dev" : "latest";
410
+ }
411
+ /**
412
+ * toCompared orders two versions by semver precedence. A release sorts after its own
413
+ * previews, and dev.10 after dev.9, because a numeric part compares as a number.
414
+ */
415
+ function toCompared(a, b) {
416
+ const [left, right] = [VERSION.exec(a), VERSION.exec(b)];
417
+ if (!left || !right) return 0;
418
+ for (let index = 1; index <= 3; index++) {
419
+ const difference = Number(left[index]) - Number(right[index]);
420
+ if (difference !== 0) return Math.sign(difference);
421
+ }
422
+ const [leftPre, rightPre] = [left[4], right[4]];
423
+ if (leftPre === void 0 || rightPre === void 0) return leftPre === rightPre ? 0 : leftPre === void 0 ? 1 : -1;
424
+ const [leftParts, rightParts] = [leftPre.split("."), rightPre.split(".")];
425
+ for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index++) {
426
+ const [l, r] = [leftParts[index], rightParts[index]];
427
+ if (l === void 0 || r === void 0) return l === void 0 ? -1 : 1;
428
+ const difference = /^\d+$/.test(l) && /^\d+$/.test(r) ? Number(l) - Number(r) : l.localeCompare(r);
429
+ if (difference !== 0) return Math.sign(difference);
430
+ }
431
+ return 0;
432
+ }
433
+ /** toRegistryTags asks npm which versions the package's tags name, or nothing when it cannot answer. */
434
+ async function toRegistryTags(signal) {
435
+ const registry = (process.env["npm_config_registry"] || "https://registry.npmjs.org").replace(/\/+$/, "");
436
+ const timeout = AbortSignal.timeout(REGISTRY_TIMEOUT_MS);
437
+ try {
438
+ const response = await fetch(`${registry}/-/package/${PACKAGE}/dist-tags`, {
439
+ headers: { Accept: "application/json" },
440
+ signal: signal ? AbortSignal.any([signal, timeout]) : timeout
441
+ });
442
+ if (!response.ok) return;
443
+ return toTags(await response.json());
444
+ } catch {
445
+ return;
446
+ }
447
+ }
448
+ /** toTags keeps the tags that name a version, since a tag's value reaches an npm command line. */
449
+ function toTags(answer) {
450
+ if (typeof answer !== "object" || answer === null) return;
451
+ const tags = {};
452
+ for (const name of ["latest", "dev"]) {
453
+ const value = answer[name];
454
+ if (typeof value === "string" && VERSION.test(value)) tags[name] = value;
455
+ }
456
+ return tags.latest === void 0 && tags.dev === void 0 ? void 0 : tags;
457
+ }
458
+ /**
459
+ * isReminderDue answers whether a daily reminder of this version may be printed, and marks it
460
+ * printed. A person told once a day is told enough, and an agent running many commands is not
461
+ * told on every one.
462
+ */
463
+ function isReminderDue(latest) {
464
+ const cache = toCache();
465
+ if (!cache) return true;
466
+ if (cache.remindedOf === latest && Date.now() - Date.parse(cache.remindedAt ?? "") < 864e5) return false;
467
+ writeCache({
468
+ ...cache,
469
+ remindedOf: latest,
470
+ remindedAt: (/* @__PURE__ */ new Date()).toISOString()
471
+ });
472
+ return true;
473
+ }
474
+ function toCachePath() {
475
+ return join(toStateDirectory(), "update-check.json");
476
+ }
477
+ function toCache() {
478
+ try {
479
+ const parsed = JSON.parse(readFileSync(toCachePath(), "utf8"));
480
+ const tags = toTags(parsed.tags);
481
+ return typeof parsed.checkedAt === "string" && tags ? {
482
+ ...parsed,
483
+ checkedAt: parsed.checkedAt,
484
+ tags
485
+ } : void 0;
486
+ } catch {
487
+ return;
488
+ }
489
+ }
490
+ function writeCache(cache) {
491
+ try {
492
+ mkdirSync(dirname(toCachePath()), { recursive: true });
493
+ writeFileSync(toCachePath(), `${JSON.stringify(cache)}\n`);
494
+ } catch {}
495
+ }
496
+ //#endregion
497
+ export { ExpectedFailure as C, TimedOut as E, version$1 as S, LockHeld as T, API_VERSION as _, isShipped as a, toSettings as b, toOutdatedTo as c, toJSON as d, toLastFailureCode as f, writeFailure as g, writeData as h, toUpdateCheckRefusal as i, toStateDirectory as l, toText as m, isReminderDue as n, toPackageDirectory as o, toPrintable as p, toCheck as r, noteNotices as s, PACKAGE as t, isJSONOutput as u, CONFIG_FILE as v, Interrupted as w, toTelemetry as x, ConfigFailure as y };
498
+
499
+ //# sourceMappingURL=check-wgnggJ45.js.map
500
+ //# debugId=9d637f5e-0cda-5582-80ff-432edc41bf8c
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]="aae61fcd-2c4e-5e7c-9063-5e90072d5171")}catch(e){}}();
3
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="e43cc9b0-2707-553a-973d-9a707e329000")}catch(e){}}();
4
4
  import module from "node:module";
5
5
  //#region src/cli.ts
6
6
  module.enableCompileCache?.();
@@ -14,7 +14,9 @@ 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-DgQuzFjb.js");
17
+ const { toCli, toParseExitCode, wasReportAttempted } = await import("./program-CYp6XF8D.js");
18
+ const { startUpdateCheck, writeUpdateNotice } = await import("./notice-CQ5Nlp80.js");
19
+ const pending = startUpdateCheck(process.argv.slice(2));
18
20
  try {
19
21
  await toCli().parseAsync(process.argv);
20
22
  } catch (error) {
@@ -22,6 +24,7 @@ try {
22
24
  if (code === void 0) throw error;
23
25
  process.exitCode = code;
24
26
  }
27
+ await writeUpdateNotice(pending, process.argv.slice(2));
25
28
  if (wasReportAttempted()) process.exit();
26
29
  } else process.stdout.write(`${answer}\n`);
27
30
  } catch (error) {
@@ -33,7 +36,7 @@ async function toCrash(error) {
33
36
  isCrashing = true;
34
37
  const isJSON = process.argv.includes("--json") || !process.stdout.isTTY;
35
38
  try {
36
- const { toReported, writeCrash } = await import("./program-DgQuzFjb.js");
39
+ const { toReported, writeCrash } = await import("./program-CYp6XF8D.js");
37
40
  writeCrash(error, isJSON);
38
41
  await toReported(error, isJSON, void 0, "fatal");
39
42
  } catch {
@@ -54,4 +57,4 @@ async function toQuickAnswer(args) {
54
57
  export {};
55
58
 
56
59
  //# sourceMappingURL=cli.js.map
57
- //# debugId=aae61fcd-2c4e-5e7c-9063-5e90072d5171
60
+ //# debugId=e43cc9b0-2707-553a-973d-9a707e329000