@n-seiji/nuthatch 0.1.5 → 0.2.1

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.
@@ -0,0 +1,639 @@
1
+ import {
2
+ isRunningAsCompiledBinary,
3
+ wantsJson,
4
+ reportFatalError,
5
+ VERSION
6
+ } from "./cli-sre95k9x.js";
7
+ import {
8
+ messageOf
9
+ } from "./cli-ph0gp56k.js";
10
+ import {
11
+ reportResult
12
+ } from "./cli-dbzx9nzg.js";
13
+ import {
14
+ EXIT_GENERAL_ERROR,
15
+ EXIT_USAGE_ERROR,
16
+ EXIT_SAFE_REJECTION,
17
+ ok,
18
+ fail
19
+ } from "./cli-77yanq37.js";
20
+ import {
21
+ parentDir
22
+ } from "./cli-s5qcjcwk.js";
23
+
24
+ // src/domain/install-method.ts
25
+ var RELEASE_ASSETS = new Map([
26
+ ["darwin-arm64", "hop-darwin-arm64"],
27
+ ["darwin-x64", "hop-darwin-x64"],
28
+ ["linux-x64", "hop-linux-x64"]
29
+ ]);
30
+ var MANAGED_TREES = [
31
+ "/Cellar/",
32
+ "/Caskroom/",
33
+ "/nix/store/",
34
+ "/aquaproj-aqua/",
35
+ "/aqua/pkgs/",
36
+ "/.proto/",
37
+ "/installs/"
38
+ ];
39
+ var MISE_TREE = "/mise/installs/";
40
+ var NODE_MODULES = "/node_modules/";
41
+ var detectInstallMethod = (facts) => {
42
+ const path = facts.compiled ? facts.executablePath : facts.scriptPath;
43
+ if (path === null) {
44
+ return { kind: "other" };
45
+ }
46
+ if (path.includes(MISE_TREE)) {
47
+ return { kind: "mise" };
48
+ }
49
+ if (!facts.compiled) {
50
+ return { kind: path.includes(NODE_MODULES) ? "package-manager" : "other" };
51
+ }
52
+ const assetName = RELEASE_ASSETS.get(`${facts.platform}-${facts.arch}`);
53
+ if (assetName === undefined || MANAGED_TREES.some((tree) => path.includes(tree))) {
54
+ return { kind: "other" };
55
+ }
56
+ return {
57
+ kind: "standalone",
58
+ binaryPath: path,
59
+ installDir: parentDir(path),
60
+ assetName
61
+ };
62
+ };
63
+ var suggestedCommand = (kind) => {
64
+ if (kind === "mise") {
65
+ return ["mise", "upgrade"];
66
+ }
67
+ return kind === "package-manager" ? ["npm", "i", "-g", "@n-seiji/nuthatch@latest"] : null;
68
+ };
69
+ var updateHint = (kind) => {
70
+ const command = suggestedCommand(kind)?.join(" ");
71
+ if (kind === "mise") {
72
+ return `Update with mise: ${command}`;
73
+ }
74
+ return kind === "package-manager" ? `Update with your package manager, e.g. ${command}` : "Update hop with the tool you installed it with.";
75
+ };
76
+
77
+ // src/domain/self-update.ts
78
+ var GITHUB_REPO = "n-seiji/nuthatch";
79
+ var GITHUB_LATEST_RELEASE_URL = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`;
80
+ var releaseAssetUrl = (version, assetName) => `https://github.com/${GITHUB_REPO}/releases/download/v${version}/${assetName}`;
81
+ var VERSION_PATTERN = /^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)$/u;
82
+ var TAG_PATTERN = /^v(?<version>\d+\.\d+\.\d+)$/u;
83
+ var SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/iu;
84
+ var parseVersion = (text) => {
85
+ const groups = VERSION_PATTERN.exec(text)?.groups;
86
+ if (groups?.major === undefined || groups.minor === undefined || groups.patch === undefined) {
87
+ throw new Error(`unrecognized version "${text}" (expected x.y.z)`);
88
+ }
89
+ return [Number(groups.major), Number(groups.minor), Number(groups.patch)];
90
+ };
91
+ var compareVersions = (a, b) => {
92
+ const [aMajor, aMinor, aPatch] = parseVersion(a);
93
+ const [bMajor, bMinor, bPatch] = parseVersion(b);
94
+ return aMajor - bMajor || aMinor - bMinor || aPatch - bPatch;
95
+ };
96
+ var versionFromTag = (tag) => {
97
+ const version = TAG_PATTERN.exec(tag)?.groups?.version;
98
+ if (version === undefined) {
99
+ throw new Error(`unrecognized release tag "${tag}" (expected vX.Y.Z)`);
100
+ }
101
+ return version;
102
+ };
103
+ var parseChecksumFile = (text, assetName) => {
104
+ const [digest, file] = text.trim().split(/\s+/u);
105
+ if (digest === undefined || !SHA256_HEX_PATTERN.test(digest)) {
106
+ return { ok: false, reason: "is malformed" };
107
+ }
108
+ const named = file?.replace(/^\*/u, "");
109
+ if (named !== undefined && named !== assetName && !named.endsWith(`/${assetName}`)) {
110
+ return { ok: false, reason: `is for "${named}", not ${assetName}` };
111
+ }
112
+ return { ok: true, digest: digest.toLowerCase() };
113
+ };
114
+ var isWriteDenied = (error) => error instanceof Error && ("code" in error) && (error.code === "EACCES" || error.code === "EPERM" || error.code === "EROFS");
115
+
116
+ // src/commands/self-update-attempt.ts
117
+ var attempt = async (run) => {
118
+ try {
119
+ return { ok: true, value: await run() };
120
+ } catch (error) {
121
+ return { ok: false, error };
122
+ }
123
+ };
124
+
125
+ // src/commands/self-update-standalone.ts
126
+ var checksumRejection = (port, assetName, checksumText, bytes) => {
127
+ const expected = parseChecksumFile(checksumText, assetName);
128
+ if (!expected.ok) {
129
+ return fail(EXIT_SAFE_REJECTION, `The checksum file ${assetName}.sha256 ${expected.reason}; refusing to install an unverified binary.`);
130
+ }
131
+ const actual = port.sha256Hex(bytes);
132
+ if (actual !== expected.digest) {
133
+ return fail(EXIT_SAFE_REJECTION, `Checksum mismatch for ${assetName} (expected ${expected.digest}, got ${actual}); refusing to install it. Nothing was changed.`);
134
+ }
135
+ return null;
136
+ };
137
+ var replaceFailureMessage = (method, error) => isWriteDenied(error) ? `Cannot write ${method.installDir}; reinstall hop somewhere writable (install.sh uses ~/.local/bin).` : `Failed to replace ${method.binaryPath}: ${messageOf(error)}`;
138
+ var replaceStandalone = async (port, term, method, base) => {
139
+ const { assetName, binaryPath } = method;
140
+ const checksumAsset = `${assetName}.sha256`;
141
+ const writable = await attempt(() => port.assertWritableDir(method.installDir));
142
+ if (!writable.ok) {
143
+ return fail(EXIT_GENERAL_ERROR, replaceFailureMessage(method, writable.error));
144
+ }
145
+ term.logStderr(`Downloading ${assetName} v${base.latest}...`);
146
+ const checksum = await attempt(() => port.downloadReleaseText(base.latest, checksumAsset));
147
+ if (!checksum.ok) {
148
+ return fail(EXIT_GENERAL_ERROR, `Failed to download ${checksumAsset}: ${messageOf(checksum.error)}`);
149
+ }
150
+ const binary = await attempt(() => port.downloadReleaseAsset(base.latest, assetName));
151
+ if (!binary.ok) {
152
+ return fail(EXIT_GENERAL_ERROR, `Failed to download ${assetName}: ${messageOf(binary.error)}`);
153
+ }
154
+ const rejection = checksumRejection(port, assetName, checksum.value, binary.value);
155
+ if (rejection !== null) {
156
+ return rejection;
157
+ }
158
+ term.logStderr(`Checksum verified. Replacing ${binaryPath}...`);
159
+ const replaced = await attempt(() => port.replaceExecutable(binaryPath, binary.value));
160
+ if (!replaced.ok) {
161
+ return fail(EXIT_GENERAL_ERROR, replaceFailureMessage(method, replaced.error));
162
+ }
163
+ term.logStderr(`Updated hop ${base.current} -> ${base.latest}.`);
164
+ return ok({ data: { ...base, action: "replaced" } });
165
+ };
166
+
167
+ // src/commands/self-update.ts
168
+ var selfUpdate = async (port, term, options) => {
169
+ const facts = await attempt(() => port.installFacts());
170
+ if (!facts.ok) {
171
+ return fail(EXIT_GENERAL_ERROR, `Cannot update hop: could not tell how it was installed (${messageOf(facts.error)}).`);
172
+ }
173
+ const method = detectInstallMethod(facts.value);
174
+ if (method.kind !== "standalone" && !options.check) {
175
+ return fail(EXIT_GENERAL_ERROR, updateHint(method.kind));
176
+ }
177
+ term.logStderr("Checking for the latest release...");
178
+ const release = await attempt(async () => {
179
+ const latest = await port.latestGithubVersion();
180
+ return {
181
+ latest,
182
+ updateAvailable: compareVersions(latest, options.currentVersion) > 0
183
+ };
184
+ });
185
+ if (!release.ok) {
186
+ return fail(EXIT_GENERAL_ERROR, `Failed to check for the latest release: ${messageOf(release.error)}`);
187
+ }
188
+ const base = {
189
+ current: options.currentVersion,
190
+ latest: release.value.latest,
191
+ updateAvailable: release.value.updateAvailable,
192
+ method: method.kind,
193
+ action: "none",
194
+ command: null
195
+ };
196
+ if (!base.updateAvailable) {
197
+ term.logStderr(`hop ${base.current} is up to date (latest release: ${base.latest}).`);
198
+ return ok({ data: base });
199
+ }
200
+ const available = `Update available: ${base.current} -> ${base.latest}.`;
201
+ if (method.kind !== "standalone") {
202
+ term.logStderr(`${available} ${updateHint(method.kind)}`);
203
+ const command = suggestedCommand(method.kind);
204
+ return ok({
205
+ data: { ...base, command: command === null ? null : [...command] }
206
+ });
207
+ }
208
+ if (options.check) {
209
+ term.logStderr(`${available} "hop --update" replaces ${method.binaryPath}.`);
210
+ return ok({ data: base });
211
+ }
212
+ return replaceStandalone(port, term, method, base);
213
+ };
214
+
215
+ // src/infra/install-facts.ts
216
+ import { realpath } from "node:fs/promises";
217
+ var realpathOrSelf = async (path) => {
218
+ try {
219
+ return await realpath(path);
220
+ } catch {
221
+ return path;
222
+ }
223
+ };
224
+ var readInstallFacts = async (source) => ({
225
+ compiled: source.compiled,
226
+ executablePath: source.compiled ? await realpathOrSelf(source.execPath) : null,
227
+ scriptPath: source.compiled || source.argv1 === undefined ? null : await realpathOrSelf(source.argv1),
228
+ platform: source.platform,
229
+ arch: source.arch
230
+ });
231
+
232
+ // src/infra/release-binary.ts
233
+ import { createHash, randomBytes } from "node:crypto";
234
+ import { constants } from "node:fs";
235
+ import { access, open, realpath as realpath2, rename, rm } from "node:fs/promises";
236
+ import { basename, dirname, join } from "node:path";
237
+ var EXECUTABLE_MODE = 493;
238
+ var TEMP_SUFFIX_BYTES = 6;
239
+ var sha256Hex = (bytes) => createHash("sha256").update(bytes).digest("hex");
240
+ var removeQuietly = async (path) => {
241
+ try {
242
+ await rm(path, { force: true });
243
+ } catch {}
244
+ };
245
+ var writeDurably = async (path, bytes) => {
246
+ const handle = await open(path, "wx", EXECUTABLE_MODE);
247
+ try {
248
+ await handle.writeFile(bytes);
249
+ await handle.chmod(EXECUTABLE_MODE);
250
+ await handle.sync();
251
+ } finally {
252
+ await handle.close();
253
+ }
254
+ };
255
+ var replaceExecutable = async (path, bytes) => {
256
+ const target = await realpath2(path);
257
+ const suffix = randomBytes(TEMP_SUFFIX_BYTES).toString("hex");
258
+ const temp = join(dirname(target), `.${basename(target)}.update-${suffix}`);
259
+ try {
260
+ await writeDurably(temp, bytes);
261
+ await rename(temp, target);
262
+ } catch (error) {
263
+ await removeQuietly(temp);
264
+ throw error;
265
+ }
266
+ };
267
+ var assertWritableDir = (dir) => access(dir, constants.W_OK | constants.X_OK);
268
+
269
+ // node_modules/valibot/dist/index.mjs
270
+ var store$4;
271
+ var DEFAULT_CONFIG = {
272
+ lang: undefined,
273
+ message: undefined,
274
+ abortEarly: undefined,
275
+ abortPipeEarly: undefined
276
+ };
277
+ function getGlobalConfig(config$1) {
278
+ if (!config$1 && !store$4)
279
+ return DEFAULT_CONFIG;
280
+ return {
281
+ lang: config$1?.lang ?? store$4?.lang,
282
+ message: config$1?.message,
283
+ abortEarly: config$1?.abortEarly ?? store$4?.abortEarly,
284
+ abortPipeEarly: config$1?.abortPipeEarly ?? store$4?.abortPipeEarly
285
+ };
286
+ }
287
+ var store$3;
288
+ function getGlobalMessage(lang) {
289
+ return store$3?.get(lang);
290
+ }
291
+ var store$2;
292
+ function getSchemaMessage(lang) {
293
+ return store$2?.get(lang);
294
+ }
295
+ var store$1;
296
+ function getSpecificMessage(reference, lang) {
297
+ return store$1?.get(reference)?.get(lang);
298
+ }
299
+ function _stringify(input) {
300
+ const type = typeof input;
301
+ if (type === "string")
302
+ return `"${input}"`;
303
+ if (type === "number" || type === "bigint" || type === "boolean")
304
+ return `${input}`;
305
+ if (type === "object" || type === "function")
306
+ return (input && Object.getPrototypeOf(input)?.constructor?.name) ?? "null";
307
+ return type;
308
+ }
309
+ function _addIssue(context, label, dataset, config$1, other) {
310
+ const input = other && "input" in other ? other.input : dataset.value;
311
+ const expected = other?.expected ?? context.expects ?? null;
312
+ const received = other?.received ?? /* @__PURE__ */ _stringify(input);
313
+ const issue = {
314
+ kind: context.kind,
315
+ type: context.type,
316
+ input,
317
+ expected,
318
+ received,
319
+ message: `Invalid ${label}: ${expected ? `Expected ${expected} but r` : "R"}eceived ${received}`,
320
+ requirement: context.requirement,
321
+ path: other?.path,
322
+ issues: other?.issues,
323
+ lang: config$1.lang,
324
+ abortEarly: config$1.abortEarly,
325
+ abortPipeEarly: config$1.abortPipeEarly
326
+ };
327
+ const isSchema = context.kind === "schema";
328
+ const message$1 = other?.message ?? context.message ?? /* @__PURE__ */ getSpecificMessage(context.reference, issue.lang) ?? (isSchema ? /* @__PURE__ */ getSchemaMessage(issue.lang) : null) ?? config$1.message ?? /* @__PURE__ */ getGlobalMessage(issue.lang);
329
+ if (message$1 !== undefined)
330
+ issue.message = typeof message$1 === "function" ? message$1(issue) : message$1;
331
+ if (isSchema)
332
+ dataset.typed = false;
333
+ if (dataset.issues)
334
+ dataset.issues.push(issue);
335
+ else
336
+ dataset.issues = [issue];
337
+ }
338
+ var _standardCache = /* @__PURE__ */ new WeakMap;
339
+ function _getStandardProps(context) {
340
+ let cached = _standardCache.get(context);
341
+ if (!cached) {
342
+ cached = {
343
+ version: 1,
344
+ vendor: "valibot",
345
+ validate(value$1) {
346
+ return context["~run"]({ value: value$1 }, /* @__PURE__ */ getGlobalConfig());
347
+ }
348
+ };
349
+ _standardCache.set(context, cached);
350
+ }
351
+ return cached;
352
+ }
353
+ function getFallback(schema, dataset, config$1) {
354
+ return typeof schema.fallback === "function" ? schema.fallback(dataset, config$1) : schema.fallback;
355
+ }
356
+ function getDefault(schema, dataset, config$1) {
357
+ return typeof schema.default === "function" ? schema.default(dataset, config$1) : schema.default;
358
+ }
359
+ function object(entries$1, message$1) {
360
+ return {
361
+ kind: "schema",
362
+ type: "object",
363
+ reference: object,
364
+ expects: "Object",
365
+ async: false,
366
+ entries: entries$1,
367
+ message: message$1,
368
+ get "~standard"() {
369
+ return /* @__PURE__ */ _getStandardProps(this);
370
+ },
371
+ "~run"(dataset, config$1) {
372
+ const input = dataset.value;
373
+ if (input && typeof input === "object") {
374
+ dataset.typed = true;
375
+ dataset.value = {};
376
+ for (const key in this.entries) {
377
+ const valueSchema = this.entries[key];
378
+ if (key in input || (valueSchema.type === "exact_optional" || valueSchema.type === "optional" || valueSchema.type === "nullish") && valueSchema.default !== undefined) {
379
+ const value$1 = key in input ? input[key] : /* @__PURE__ */ getDefault(valueSchema);
380
+ const valueDataset = valueSchema["~run"]({ value: value$1 }, config$1);
381
+ if (valueDataset.issues) {
382
+ const pathItem = {
383
+ type: "object",
384
+ origin: "value",
385
+ input,
386
+ key,
387
+ value: value$1
388
+ };
389
+ for (const issue of valueDataset.issues) {
390
+ if (issue.path)
391
+ issue.path.unshift(pathItem);
392
+ else
393
+ issue.path = [pathItem];
394
+ dataset.issues?.push(issue);
395
+ }
396
+ if (!dataset.issues)
397
+ dataset.issues = valueDataset.issues;
398
+ if (config$1.abortEarly) {
399
+ dataset.typed = false;
400
+ break;
401
+ }
402
+ }
403
+ if (!valueDataset.typed)
404
+ dataset.typed = false;
405
+ dataset.value[key] = valueDataset.value;
406
+ } else if (valueSchema.fallback !== undefined)
407
+ dataset.value[key] = /* @__PURE__ */ getFallback(valueSchema);
408
+ else if (valueSchema.type !== "exact_optional" && valueSchema.type !== "optional" && valueSchema.type !== "nullish") {
409
+ _addIssue(this, "key", dataset, config$1, {
410
+ input: undefined,
411
+ expected: `"${key}"`,
412
+ path: [{
413
+ type: "object",
414
+ origin: "key",
415
+ input,
416
+ key,
417
+ value: input[key]
418
+ }]
419
+ });
420
+ if (config$1.abortEarly)
421
+ break;
422
+ }
423
+ }
424
+ } else
425
+ _addIssue(this, "type", dataset, config$1);
426
+ return dataset;
427
+ }
428
+ };
429
+ }
430
+ function string(message$1) {
431
+ return {
432
+ kind: "schema",
433
+ type: "string",
434
+ reference: string,
435
+ expects: "string",
436
+ async: false,
437
+ message: message$1,
438
+ get "~standard"() {
439
+ return /* @__PURE__ */ _getStandardProps(this);
440
+ },
441
+ "~run"(dataset, config$1) {
442
+ if (typeof dataset.value === "string")
443
+ dataset.typed = true;
444
+ else
445
+ _addIssue(this, "type", dataset, config$1);
446
+ return dataset;
447
+ }
448
+ };
449
+ }
450
+ function safeParse(schema, input, config$1) {
451
+ const dataset = schema["~run"]({ value: input }, /* @__PURE__ */ getGlobalConfig(config$1));
452
+ return {
453
+ typed: dataset.typed,
454
+ success: !dataset.issues,
455
+ output: dataset.value,
456
+ issues: dataset.issues
457
+ };
458
+ }
459
+
460
+ // src/infra/capped-body.ts
461
+ var tooLarge = (maxBytes) => new Error(`the response is larger than ${maxBytes} bytes`);
462
+ var concatenate = (chunks, total) => {
463
+ const bytes = new Uint8Array(total);
464
+ let offset = 0;
465
+ for (const chunk of chunks) {
466
+ bytes.set(chunk, offset);
467
+ offset += chunk.length;
468
+ }
469
+ return bytes;
470
+ };
471
+ var readCapped = async (response, maxBytes) => {
472
+ const declared = Math.trunc(Number(response.headers.get("content-length") ?? ""));
473
+ if (declared > maxBytes) {
474
+ await response.body?.cancel();
475
+ throw tooLarge(maxBytes);
476
+ }
477
+ const chunks = [];
478
+ let received = 0;
479
+ for await (const chunk of response.body ?? []) {
480
+ received += chunk.length;
481
+ if (received > maxBytes) {
482
+ throw tooLarge(maxBytes);
483
+ }
484
+ chunks.push(chunk);
485
+ }
486
+ return concatenate(chunks, received);
487
+ };
488
+
489
+ // src/infra/release-http.ts
490
+ var METADATA_TIMEOUT_MS = 15000;
491
+ var ASSET_TIMEOUT_MS = 300000;
492
+ var METADATA_MAX_BYTES = 1048576;
493
+ var CHECKSUM_MAX_BYTES = 4096;
494
+ var BINARY_MAX_BYTES = 268435456;
495
+ var GITHUB_API_ORIGIN = "https://api.github.com/";
496
+ var HTTP_FORBIDDEN = 403;
497
+ var HTTP_TOO_MANY_REQUESTS = 429;
498
+ var GithubLatestSchema = object({ tag_name: string() });
499
+ var DECODER = new TextDecoder;
500
+ var send = async (fetcher, url, { headers, timeoutMs }) => {
501
+ try {
502
+ return await fetcher(url, {
503
+ headers,
504
+ redirect: "follow",
505
+ signal: AbortSignal.timeout(timeoutMs)
506
+ });
507
+ } catch (error) {
508
+ throw new Error(`GET ${url} failed: ${messageOf(error)}`, { cause: error });
509
+ }
510
+ };
511
+ var rateLimitHint = (url, response) => {
512
+ const refused = response.status === HTTP_FORBIDDEN || response.status === HTTP_TOO_MANY_REQUESTS;
513
+ const exhausted = refused && response.headers.get("x-ratelimit-remaining") === "0";
514
+ return exhausted && url.startsWith(GITHUB_API_ORIGIN) ? " (the unauthenticated GitHub API rate limit was hit; try again later)" : "";
515
+ };
516
+ var discardBody = async (response) => {
517
+ try {
518
+ await response.body?.cancel();
519
+ } catch {}
520
+ };
521
+ var get = async (fetcher, url, options) => {
522
+ const response = await send(fetcher, url, options);
523
+ if (!response.ok) {
524
+ await discardBody(response);
525
+ throw new Error(`GET ${url} failed: HTTP ${response.status}${rateLimitHint(url, response)}`);
526
+ }
527
+ if (response.url.startsWith("http://")) {
528
+ await discardBody(response);
529
+ throw new Error(`GET ${url} failed: redirected to ${response.url}, which is not HTTPS`);
530
+ }
531
+ return response;
532
+ };
533
+ var readBody = async (url, read) => {
534
+ try {
535
+ return await read();
536
+ } catch (error) {
537
+ throw new Error(`GET ${url} failed while reading the response: ${messageOf(error)}`, {
538
+ cause: error
539
+ });
540
+ }
541
+ };
542
+ var getBytes = async (fetcher, url, options) => {
543
+ const response = await get(fetcher, url, options);
544
+ return await readBody(url, () => readCapped(response, options.maxBytes));
545
+ };
546
+ var getJson = async (fetcher, url, schema, options) => {
547
+ const bytes = await getBytes(fetcher, url, options);
548
+ const body = await readBody(url, () => JSON.parse(DECODER.decode(bytes)));
549
+ const parsed = safeParse(schema, body);
550
+ if (!parsed.success) {
551
+ throw new Error(`GET ${url} returned an unexpected response`);
552
+ }
553
+ return parsed.output;
554
+ };
555
+ var createReleaseHttp = (userAgent, fetcher = fetch) => ({
556
+ async latestGithubVersion() {
557
+ const release = await getJson(fetcher, GITHUB_LATEST_RELEASE_URL, GithubLatestSchema, {
558
+ headers: {
559
+ Accept: "application/vnd.github+json",
560
+ "User-Agent": userAgent
561
+ },
562
+ timeoutMs: METADATA_TIMEOUT_MS,
563
+ maxBytes: METADATA_MAX_BYTES
564
+ });
565
+ return versionFromTag(release.tag_name);
566
+ },
567
+ async downloadReleaseAsset(version, assetName) {
568
+ return await getBytes(fetcher, releaseAssetUrl(version, assetName), {
569
+ headers: { Accept: "application/octet-stream", "User-Agent": userAgent },
570
+ timeoutMs: ASSET_TIMEOUT_MS,
571
+ maxBytes: BINARY_MAX_BYTES
572
+ });
573
+ },
574
+ async downloadReleaseText(version, assetName) {
575
+ const bytes = await getBytes(fetcher, releaseAssetUrl(version, assetName), {
576
+ headers: { Accept: "text/plain", "User-Agent": userAgent },
577
+ timeoutMs: METADATA_TIMEOUT_MS,
578
+ maxBytes: CHECKSUM_MAX_BYTES
579
+ });
580
+ return DECODER.decode(bytes);
581
+ }
582
+ });
583
+
584
+ // src/infra/self-update.ts
585
+ var createSelfUpdatePort = ({
586
+ compiled,
587
+ version
588
+ }) => ({
589
+ installFacts: () => readInstallFacts({
590
+ compiled,
591
+ execPath: process.execPath,
592
+ argv1: process.argv[1],
593
+ platform: process.platform,
594
+ arch: process.arch
595
+ }),
596
+ ...createReleaseHttp(`hop/${version}`),
597
+ sha256Hex,
598
+ assertWritableDir,
599
+ replaceExecutable
600
+ });
601
+
602
+ // src/cli-update.ts
603
+ var CHECK_FLAG = "--check";
604
+ var JSON_FLAG = "--json";
605
+ var UPDATE_FLAGS = new Set([CHECK_FLAG, JSON_FLAG]);
606
+ var parseUpdateArgs = (tokens) => {
607
+ const stray = tokens.filter((token) => !UPDATE_FLAGS.has(token));
608
+ if (stray.length > 0) {
609
+ return {
610
+ ok: false,
611
+ json: wantsJson(tokens),
612
+ message: `Unknown argument for --update: ${stray.join(" ")}. Usage: hop --update [--check] [--json]`
613
+ };
614
+ }
615
+ return { ok: true, check: tokens.includes(CHECK_FLAG), json: tokens.includes(JSON_FLAG) };
616
+ };
617
+ var runUpdate = async (rawArgs, term) => {
618
+ try {
619
+ const parsed = parseUpdateArgs(rawArgs.slice(1));
620
+ if (!parsed.ok) {
621
+ reportResult("update", fail(EXIT_USAGE_ERROR, parsed.message), parsed.json);
622
+ return;
623
+ }
624
+ const port = createSelfUpdatePort({
625
+ compiled: isRunningAsCompiledBinary(),
626
+ version: VERSION
627
+ });
628
+ const result = await selfUpdate(port, term, {
629
+ currentVersion: VERSION,
630
+ check: parsed.check
631
+ });
632
+ reportResult("update", result, parsed.json);
633
+ } catch (error) {
634
+ reportFatalError("update", wantsJson(rawArgs), error);
635
+ }
636
+ };
637
+ export {
638
+ runUpdate
639
+ };