@hardfin/cli 0.1.1-dev.67 → 0.1.1-dev.69

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.
@@ -122,7 +173,9 @@ hardfin file get file_7hq2mx9pkr4stz8w --output - | wc -c
122
173
 
123
174
  Send a whole body as JSON with `--input`, from a file or from standard input. A flag given as
124
175
  well replaces its field, and a required field the body carries needs no flag. A value with a
125
- comma in it, which a `key=value` flag cannot carry, goes through `--input`.
176
+ comma in it, which a `key=value` flag cannot carry, goes through `--input`. A field the API
177
+ document does not define for the command is refused before anything is sent, with where it sits
178
+ and what is taken there.
126
179
 
127
180
  ```sh
128
181
  hardfin location create --input location.json
@@ -167,7 +220,7 @@ otherwise have to read. Pipe a result through `jq .` to read it indented.
167
220
  | `requestId` | The API's request ID, or null when the request never reached it |
168
221
  | `status` | The HTTP status, or null for a failure caught before any request is made |
169
222
 
170
- Four codes cover failures that are easy to mistake for something else.
223
+ Five codes cover failures that are easy to mistake for something else.
171
224
 
172
225
  | `code` | Happens when |
173
226
  | --------------------- | ---------------------------------------------------------------------------------------------------- |
@@ -175,6 +228,7 @@ Four codes cover failures that are easy to mistake for something else.
175
228
  | `unexpected_response` | A 200 whose body is not the API's JSON, such as a proxy's page. It is never an empty result |
176
229
  | `timed_out` | The server did not begin to answer within 60 seconds, or 300 for an upload |
177
230
  | `file_error` | This machine refused a path the command was given |
231
+ | `cli_outdated` | The API no longer serves the version of the endpoint this CLI speaks. Only `hardfin update` fixes it |
178
232
 
179
233
  A change that timed out may still have been made, and the message says so. Read the record
180
234
  before sending the change again.
@@ -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]="e9a5da76-6485-5cc1-9d09-55076e5c9b22")}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/output/writer.ts
12
+ /**
13
+ * toText reads a value a caller supplied, which arrives typed as unknown. An object would
14
+ * otherwise reach a request as the text "[object Object]".
15
+ */
16
+ function toText(value) {
17
+ if (typeof value === "string") return value;
18
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
19
+ if (value === void 0 || value === null) return "";
20
+ return JSON.stringify(value);
21
+ }
22
+ /** Every control character but a newline and a tab, which are the two a line of text needs. */
23
+ const CONTROL = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
24
+ /**
25
+ * toPrintable makes text safe to show at a terminal. A name or a message from the API can
26
+ * hold an escape sequence, which a terminal would act on. Each such character is written
27
+ * as its code, so the text reads as what it holds.
28
+ */
29
+ function toPrintable(text) {
30
+ return text.replace(CONTROL, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
31
+ }
32
+ /**
33
+ * toJSON writes a value for whoever is reading the stream. A person at a terminal gets it
34
+ * indented. A program gets it on one line, because the indentation of a page of records is
35
+ * close to half of what an agent would otherwise have to read.
36
+ */
37
+ function toJSON(value, stream) {
38
+ return stream.isTTY ? JSON.stringify(value, null, 2) : JSON.stringify(value);
39
+ }
40
+ /** writeData prints a command's result on stdout. */
41
+ function writeData(value) {
42
+ if (typeof value === "string") {
43
+ const text = toPrintable(value);
44
+ process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
45
+ return;
46
+ }
47
+ process.stdout.write(`${toJSON(value ?? null, process.stdout)}\n`);
48
+ }
49
+ /** The code of the last failure written, which decides whether the CLI checks for an update. */
50
+ let lastFailureCode;
51
+ /** toLastFailureCode answers the code of the last failure this run wrote. */
52
+ function toLastFailureCode() {
53
+ return lastFailureCode;
54
+ }
55
+ /** writeFailure prints why a command failed on stderr, with a code only a machine reading JSON sees. */
56
+ function writeFailure(code, message, isJSON, errors, requestId, status) {
57
+ lastFailureCode = code;
58
+ if (!isJSON) {
59
+ process.stderr.write(`error: ${toPrintable(message)}${status === void 0 ? "" : ` (HTTP ${status})`}\n`);
60
+ for (const entry of errors?.slice(1) ?? []) process.stderr.write(` ${toPrintable(entry.error)} (${entry.statusCode})\n`);
61
+ if (requestId) process.stderr.write(` request ${toPrintable(requestId)}\n`);
62
+ return;
63
+ }
64
+ const payload = {
65
+ code,
66
+ message,
67
+ errors: errors ?? [{
68
+ error: message,
69
+ statusCode: 0
70
+ }],
71
+ requestId: requestId ?? null,
72
+ status: status ?? null
73
+ };
74
+ process.stderr.write(`${toJSON(payload, process.stderr)}\n`);
75
+ }
76
+ /** isJSONOutput decides whether this invocation prints machine-readable output. */
77
+ function isJSONOutput(flags) {
78
+ if (flags["json"] === true) return true;
79
+ return !process.stdout.isTTY;
80
+ }
81
+ /** version is what this build reports, read from the published package manifest. */
82
+ const version$1 = createRequire(import.meta.url)("../package.json").version;
83
+ //#endregion
84
+ //#region src/report/expected.ts
85
+ /**
86
+ * ExpectedFailure is a failure the CLI anticipated: the person's input, their network, their
87
+ * machine, or an answer from the API. It is shown to the person and never reported, and every
88
+ * error that does not extend it is reported as a defect.
89
+ */
90
+ var ExpectedFailure = class extends Error {};
91
+ /** LockHeld is another hardfin command holding the credential lock for too long. */
92
+ var LockHeld = class extends ExpectedFailure {
93
+ constructor() {
94
+ super("another hardfin command is holding the credential lock");
95
+ this.name = "LockHeld";
96
+ }
97
+ };
98
+ /**
99
+ * Interrupted is a person stopping the CLI with Ctrl-C. It is something they chose to do,
100
+ * not a failure, so the CLI stops quietly with the exit code a shell gives an interrupt.
101
+ */
102
+ var Interrupted = class extends ExpectedFailure {
103
+ constructor() {
104
+ super("interrupted");
105
+ this.name = "Interrupted";
106
+ }
107
+ };
108
+ /** TimedOut is something the CLI waited on for longer than it allows, such as the browser. */
109
+ var TimedOut = class extends ExpectedFailure {
110
+ constructor(message) {
111
+ super(message);
112
+ this.name = "TimedOut";
113
+ }
114
+ };
115
+ /** The values that mean no, for a variable that turns something on or off. */
116
+ const OFF = /* @__PURE__ */ new Set([
117
+ "0",
118
+ "false",
119
+ "off",
120
+ "no"
121
+ ]);
122
+ /**
123
+ * toTelemetry decides whether defects are reported. A person's opt out wins over everything,
124
+ * and a CLI run from a checkout reports only when its developer asks it to.
125
+ */
126
+ function toTelemetry(env = process.env, isCheckout = isRunFromCheckout()) {
127
+ const dsn = env["HARDFIN_SENTRY_DSN"] || "https://a4819e652e9fa63c0ab67e9febe44565@o1388102.ingest.us.sentry.io/4512176532684800";
128
+ const base = {
129
+ dsn,
130
+ environment: isCheckout ? "local" : version$1.includes("-dev.") ? "preview" : "production",
131
+ release: toRelease(version$1)
132
+ };
133
+ if (env["DO_NOT_TRACK"] && !OFF.has(env["DO_NOT_TRACK"].toLowerCase())) return {
134
+ ...base,
135
+ enabled: false,
136
+ from: "DO_NOT_TRACK"
137
+ };
138
+ const asked = env["HARDFIN_TELEMETRY"]?.toLowerCase();
139
+ if (asked !== void 0 && OFF.has(asked)) return {
140
+ ...base,
141
+ enabled: false,
142
+ from: "HARDFIN_TELEMETRY"
143
+ };
144
+ if (!isDsn(dsn)) return {
145
+ ...base,
146
+ enabled: false,
147
+ from: "HARDFIN_SENTRY_DSN is not a valid DSN"
148
+ };
149
+ if (asked !== void 0) return {
150
+ ...base,
151
+ enabled: true,
152
+ from: "HARDFIN_TELEMETRY"
153
+ };
154
+ return isCheckout ? {
155
+ ...base,
156
+ enabled: false,
157
+ from: "run from a checkout"
158
+ } : {
159
+ ...base,
160
+ enabled: true,
161
+ from: "default"
162
+ };
163
+ }
164
+ /**
165
+ * isRunFromCheckout answers whether this CLI runs from a clone of its repository, as npm link
166
+ * and node dist/cli.js do. The published package holds dist and nothing beside it.
167
+ */
168
+ function isRunFromCheckout() {
169
+ return existsSync(fileURLToPath(new URL("../src", import.meta.url)));
170
+ }
171
+ /** isDsn answers whether a DSN can be read as the URL of a Sentry project. */
172
+ function isDsn(dsn) {
173
+ try {
174
+ return new URL(dsn).host !== "";
175
+ } catch {
176
+ return false;
177
+ }
178
+ }
179
+ /**
180
+ * toRelease names a version as Sentry knows it. Sentry refuses a slash in a release name, so
181
+ * the scoped package name @hardfin/cli cannot be used, and hardfin-cli stands in for it.
182
+ */
183
+ function toRelease(of) {
184
+ return `hardfin-cli@${of}`;
185
+ }
186
+ //#endregion
187
+ //#region src/config/settings.ts
188
+ /** The API version this build was written against, sent on every request. */
189
+ const API_VERSION = "2026-10-21";
190
+ /** The file a local build reads its overrides from, in the working directory. */
191
+ const CONFIG_FILE = "config.local.json";
192
+ const DEFAULT_API_URL = "https://api.hardfin.com/v2";
193
+ /**
194
+ * The client Hardfin seeded for this CLI. The key is generated from a fixed identifier, so
195
+ * it names the same first-party client in every environment.
196
+ */
197
+ const DEFAULT_CLIENT_ID = "oacl_10e2etq297nhjfhb";
198
+ const DEFAULT_ENV_FILE = ".env";
199
+ const FileSettings = z.strictObject({
200
+ apiUrl: z.string().optional(),
201
+ apiKey: z.string().optional(),
202
+ clientId: z.string().optional(),
203
+ issuerUrl: z.string().optional()
204
+ });
205
+ /** ConfigFailure is a config file that cannot be read or does not match the schema. */
206
+ var ConfigFailure = class extends ExpectedFailure {
207
+ constructor(message) {
208
+ super(message);
209
+ this.name = "ConfigFailure";
210
+ }
211
+ };
212
+ /** toSettings resolves what this invocation talks to, and where each value came from. */
213
+ function toSettings(flags = {}, directory = process.cwd()) {
214
+ const fromEnvFile = loadEnvFile(directory);
215
+ const file = toFileSettings(directory);
216
+ const sources = {};
217
+ const pick = (key, flag, variable, fromFile, fallback) => {
218
+ const [value, source] = toLayer(flag, process.env[variable], fromEnvFile.has(variable), fromFile, fallback);
219
+ sources[key] = source;
220
+ return value;
221
+ };
222
+ const apiUrl = toTrimmedUrl(pick("apiUrl", flags.apiUrl, "HARDFIN_API_URL", file.apiUrl, DEFAULT_API_URL) ?? DEFAULT_API_URL);
223
+ return {
224
+ settings: {
225
+ apiUrl,
226
+ apiKey: pick("apiKey", flags.apiKey, "HARDFIN_API_KEY", file.apiKey),
227
+ clientId: pick("clientId", flags.clientId, "HARDFIN_CLIENT_ID", file.clientId, DEFAULT_CLIENT_ID) ?? DEFAULT_CLIENT_ID,
228
+ issuerUrl: pick("issuerUrl", flags.issuerUrl, "HARDFIN_ISSUER_URL", file.issuerUrl, toOrigin(apiUrl)) ?? ""
229
+ },
230
+ sources
231
+ };
232
+ }
233
+ function toLayer(flag, environment, isFromEnvFile, file, fallback) {
234
+ if (flag) return [flag, "flag"];
235
+ if (environment) return [environment, isFromEnvFile ? "env file" : "environment"];
236
+ if (file) return [file, "config file"];
237
+ return [fallback, "default"];
238
+ }
239
+ /** toFileSettings reads the local override file, which a local build is expected to have. */
240
+ function toFileSettings(directory) {
241
+ const path = resolve(directory, CONFIG_FILE);
242
+ if (!existsSync(path)) return {};
243
+ let parsed;
244
+ try {
245
+ parsed = JSON.parse(readFileSync(path, "utf8"));
246
+ } catch {
247
+ throw new ConfigFailure(`${CONFIG_FILE} does not hold JSON`);
248
+ }
249
+ const result = FileSettings.safeParse(parsed);
250
+ if (!result.success) {
251
+ const issue = result.error.issues[0];
252
+ throw new ConfigFailure(`${CONFIG_FILE} is not valid: ${issue?.path.join(".") || "root"} ${issue?.message ?? ""}`.trim());
253
+ }
254
+ return result.data;
255
+ }
256
+ /** The variables an env file may set, which are the settings and nothing else the CLI reads. */
257
+ const SETTING_VARIABLES = /* @__PURE__ */ new Set([
258
+ "HARDFIN_API_URL",
259
+ "HARDFIN_API_KEY",
260
+ "HARDFIN_CLIENT_ID",
261
+ "HARDFIN_ISSUER_URL"
262
+ ]);
263
+ /**
264
+ * isTakenFromFile answers whether an env file may set a variable. A file in a folder supplies
265
+ * the settings, and may turn error reports off. Where reports go, where a sign in is kept,
266
+ * and anything a started program reads are for the person to set themselves.
267
+ */
268
+ function isTakenFromFile(name, value) {
269
+ if (SETTING_VARIABLES.has(name)) return true;
270
+ if (name === "DO_NOT_TRACK") return !OFF.has(value.toLowerCase());
271
+ return name === "HARDFIN_TELEMETRY" && OFF.has(value.toLowerCase());
272
+ }
273
+ /**
274
+ * loadEnvFile reads a .env beside the command, so a local build needs no exports, and
275
+ * answers which variables it supplied. An exported variable is left alone, so a shell
276
+ * export still wins over the file.
277
+ */
278
+ function loadEnvFile(directory) {
279
+ const path = process.env["HARDFIN_ENV_FILE"] ?? resolve(directory, DEFAULT_ENV_FILE);
280
+ if (!existsSync(path)) return /* @__PURE__ */ new Set();
281
+ let held;
282
+ try {
283
+ held = parseEnv(readFileSync(path, "utf8"));
284
+ } catch {
285
+ throw new ConfigFailure(`${path} cannot be read as an env file`);
286
+ }
287
+ const supplied = /* @__PURE__ */ new Set();
288
+ for (const [name, value] of Object.entries(held)) {
289
+ if (value === void 0 || process.env[name] !== void 0 || !isTakenFromFile(name, value)) continue;
290
+ process.env[name] = value;
291
+ supplied.add(name);
292
+ }
293
+ return supplied;
294
+ }
295
+ function toTrimmedUrl(url) {
296
+ return url.replace(/\/+$/, "");
297
+ }
298
+ /** toOrigin drops the API's path, because the authorization server sits at the host root. */
299
+ function toOrigin(apiUrl) {
300
+ try {
301
+ return new URL(apiUrl).origin;
302
+ } catch {
303
+ return apiUrl;
304
+ }
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 { toPrintable as C, writeFailure as E, toLastFailureCode as S, writeData as T, LockHeld as _, isShipped as a, isJSONOutput as b, toOutdatedTo as c, CONFIG_FILE as d, ConfigFailure as f, Interrupted as g, ExpectedFailure as h, toUpdateCheckRefusal as i, toStateDirectory as l, toTelemetry as m, isReminderDue as n, toPackageDirectory as o, toSettings as p, toCheck as r, noteNotices as s, PACKAGE as t, API_VERSION as u, TimedOut as v, toText as w, toJSON as x, version$1 as y };
498
+
499
+ //# sourceMappingURL=check-CDkHR_Qt.js.map
500
+ //# debugId=e9a5da76-6485-5cc1-9d09-55076e5c9b22
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]="61135054-c950-5fd7-a811-e4c83d75d885")}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]="6ad4927e-4221-5401-ac3e-392b1fcb2a19")}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-D8yu8l1H.js");
17
+ const { toCli, toParseExitCode, wasReportAttempted } = await import("./program-eT-hgKX0.js");
18
+ const { startUpdateCheck, writeUpdateNotice } = await import("./notice-Bq9ifPV6.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-D8yu8l1H.js");
39
+ const { toReported, writeCrash } = await import("./program-eT-hgKX0.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=61135054-c950-5fd7-a811-e4c83d75d885
60
+ //# debugId=6ad4927e-4221-5401-ac3e-392b1fcb2a19