@n-seiji/nuthatch 0.1.4 → 0.1.7

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,891 @@
1
+ import {
2
+ isRunningAsCompiledBinary,
3
+ wantsJson,
4
+ reportFatalError,
5
+ firstExecutable,
6
+ VERSION
7
+ } from "./cli-3ya15je0.js";
8
+ import {
9
+ parentDir,
10
+ baseName,
11
+ absoluteCandidates,
12
+ splitPathEnv,
13
+ messageOf
14
+ } from "./cli-qrxgkskr.js";
15
+ import {
16
+ reportResult
17
+ } from "./cli-dbzx9nzg.js";
18
+ import {
19
+ EXIT_GENERAL_ERROR,
20
+ EXIT_USAGE_ERROR,
21
+ EXIT_SAFE_REJECTION,
22
+ ok,
23
+ fail
24
+ } from "./cli-2bhb9p0x.js";
25
+
26
+ // src/domain/install-roots.ts
27
+ var MANAGER_ROOTS = [
28
+ {
29
+ markers: ["/Cellar/", "/Caskroom/"],
30
+ manager: "Homebrew",
31
+ update: '"brew upgrade"',
32
+ ownsNpmPrefix: true
33
+ },
34
+ {
35
+ markers: ["/nix/store/"],
36
+ manager: "Nix",
37
+ update: "Nix (the store is read-only)",
38
+ ownsNpmPrefix: true
39
+ },
40
+ {
41
+ markers: ["/aquaproj-aqua/", "/aqua/pkgs/"],
42
+ manager: "aqua",
43
+ update: "aqua",
44
+ ownsNpmPrefix: false
45
+ },
46
+ { markers: ["/.proto/tools/"], manager: "proto", update: "proto", ownsNpmPrefix: false }
47
+ ];
48
+ var reasonWithin = (path, roots) => {
49
+ const root = roots.find(({ markers }) => markers.some((marker) => path.includes(marker)));
50
+ return root === undefined ? null : `installed by ${root.manager} (${path}); update it with ${root.update}`;
51
+ };
52
+ var managerRootReason = (binaryPath) => reasonWithin(binaryPath, MANAGER_ROOTS);
53
+ var managerNpmPrefixReason = (prefix) => reasonWithin(prefix, MANAGER_ROOTS.filter(({ ownsNpmPrefix }) => ownsNpmPrefix));
54
+
55
+ // src/domain/self-update.ts
56
+ var NPM_PACKAGE = "@n-seiji/nuthatch";
57
+ var NPM_PACKAGE_LATEST = `${NPM_PACKAGE}@latest`;
58
+ var GITHUB_REPO = "n-seiji/nuthatch";
59
+ var GITHUB_LATEST_RELEASE_URL = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`;
60
+ var NPM_LATEST_URL = `https://registry.npmjs.org/${NPM_PACKAGE.replace("/", "%2F")}/latest`;
61
+ var releaseAssetUrl = (version, assetName) => `https://github.com/${GITHUB_REPO}/releases/download/v${version}/${assetName}`;
62
+ var VERSION_PATTERN = /^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)$/u;
63
+ var TAG_PATTERN = /^v(?<version>\d+\.\d+\.\d+)$/u;
64
+ var SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/iu;
65
+ var parseVersion = (text) => {
66
+ const groups = VERSION_PATTERN.exec(text)?.groups;
67
+ if (groups?.major === undefined || groups.minor === undefined || groups.patch === undefined) {
68
+ throw new Error(`unrecognized version "${text}" (expected x.y.z)`);
69
+ }
70
+ return [Number(groups.major), Number(groups.minor), Number(groups.patch)];
71
+ };
72
+ var compareVersions = (a, b) => {
73
+ const [aMajor, aMinor, aPatch] = parseVersion(a);
74
+ const [bMajor, bMinor, bPatch] = parseVersion(b);
75
+ return aMajor - bMajor || aMinor - bMinor || aPatch - bPatch;
76
+ };
77
+ var requireVersion = (text) => {
78
+ parseVersion(text);
79
+ return text;
80
+ };
81
+ var versionFromTag = (tag) => {
82
+ const version = TAG_PATTERN.exec(tag)?.groups?.version;
83
+ if (version === undefined) {
84
+ throw new Error(`unrecognized release tag "${tag}" (expected vX.Y.Z)`);
85
+ }
86
+ return version;
87
+ };
88
+ var parseChecksumFile = (text, assetName) => {
89
+ const [digest, file] = text.trim().split(/\s+/u);
90
+ if (digest === undefined || !SHA256_HEX_PATTERN.test(digest)) {
91
+ return { ok: false, reason: "is malformed" };
92
+ }
93
+ const named = file?.replace(/^\*/u, "");
94
+ if (named !== undefined && named !== assetName && !named.endsWith(`/${assetName}`)) {
95
+ return { ok: false, reason: `is for "${named}", not ${assetName}` };
96
+ }
97
+ return { ok: true, digest: digest.toLowerCase() };
98
+ };
99
+ var pathExecutableCandidates = (name, pathEnv, preferredDir = null) => absoluteCandidates([...preferredDir === null ? [] : [preferredDir], ...splitPathEnv(pathEnv)], name);
100
+ var isWriteDenied = (error) => error instanceof Error && ("code" in error) && (error.code === "EACCES" || error.code === "EPERM" || error.code === "EROFS");
101
+
102
+ // src/domain/install-script.ts
103
+ var NPX_MARKER = "/_npx/";
104
+ var BUNX_MARKER = "/bunx-";
105
+ var BUN_GLOBAL_MARKER = "/.bun/install/global/node_modules/";
106
+ var NPM_GLOBAL_MARKER = `/lib/node_modules/${NPM_PACKAGE}/`;
107
+ var NODE_MODULES_MARKER = `/node_modules/${NPM_PACKAGE}/`;
108
+ var PNPM_GLOBAL_MARKER = "/pnpm/global/";
109
+ var YARN_GLOBAL_MARKER = "/yarn/global/";
110
+ var unsupported = (reason) => ({ kind: "unsupported", reason });
111
+ var npmGlobalPrefixOf = (scriptPath) => {
112
+ const index = scriptPath.lastIndexOf(NPM_GLOBAL_MARKER);
113
+ return index > 0 ? scriptPath.slice(0, index) : null;
114
+ };
115
+ var otherNodeModulesReason = (scriptPath) => {
116
+ if (scriptPath.includes(PNPM_GLOBAL_MARKER)) {
117
+ return `installed globally with pnpm, which hop does not drive; run "pnpm add -g ${NPM_PACKAGE_LATEST}" yourself`;
118
+ }
119
+ if (scriptPath.includes(YARN_GLOBAL_MARKER)) {
120
+ return `installed globally with yarn, which hop does not drive; run "yarn global add ${NPM_PACKAGE_LATEST}" yourself`;
121
+ }
122
+ return "installed as a project dependency (a local node_modules); update it in that project";
123
+ };
124
+ var detectScriptInstall = (scriptPath, npmGlobalPrefix) => {
125
+ if (scriptPath.includes(NPX_MARKER)) {
126
+ return unsupported("running through npx, which already fetches the published version on every run");
127
+ }
128
+ if (scriptPath.includes(BUNX_MARKER)) {
129
+ return unsupported("running through bunx, which already fetches the published version on every run");
130
+ }
131
+ if (scriptPath.includes(BUN_GLOBAL_MARKER)) {
132
+ return { kind: "bun" };
133
+ }
134
+ if (npmGlobalPrefix !== null) {
135
+ const managedBy = managerNpmPrefixReason(npmGlobalPrefix);
136
+ return managedBy === null ? { kind: "npm", prefix: npmGlobalPrefix } : unsupported(managedBy);
137
+ }
138
+ if (scriptPath.includes(NODE_MODULES_MARKER)) {
139
+ return unsupported(otherNodeModulesReason(scriptPath));
140
+ }
141
+ return unsupported("running from a source checkout; update it with git (git pull)");
142
+ };
143
+
144
+ // src/domain/mise-tool.ts
145
+ var MISE_WALK_LIMIT = 8;
146
+ var HOP_BACKEND_PATTERN = /^[a-z0-9_-]+:@?n-seiji\/nuthatch$/iu;
147
+ var HOP_TOOL_DIR_PATTERN = /(?:^|-)n-seiji-nuthatch$/iu;
148
+ var parseMiseBackendToml = (text) => {
149
+ const match = /^\s*full\s*=\s*"(?<tool>[^"]+)"\s*$/mu.exec(text);
150
+ return match?.groups?.tool ?? null;
151
+ };
152
+ var parseLegacyMiseBackend = (text) => {
153
+ const lines = text.split(`
154
+ `).map((line) => line.trim()).filter((line) => line.length > 0);
155
+ return lines.at(-1) ?? null;
156
+ };
157
+ var withoutToolOptions = (backend) => backend.replace(/\[[^\]]*\]$/u, "");
158
+ var isHopBackend = (backend) => HOP_BACKEND_PATTERN.test(backend);
159
+ var isHopToolDirName = (name) => HOP_TOOL_DIR_PATTERN.test(name);
160
+ var ancestorDirs = (path, limit) => {
161
+ const dirs = [];
162
+ let dir = parentDir(path);
163
+ while (dirs.length < limit && dir !== "/" && dir !== ".") {
164
+ dirs.push(dir);
165
+ dir = parentDir(dir);
166
+ }
167
+ return dirs;
168
+ };
169
+ var miseToolDir = (path) => ancestorDirs(path, MISE_WALK_LIMIT).find((dir) => baseName(parentDir(dir)) === "installs") ?? null;
170
+
171
+ // src/domain/install-method.ts
172
+ var RELEASE_ASSETS = new Map([
173
+ ["darwin-arm64", "hop-darwin-arm64"],
174
+ ["darwin-x64", "hop-darwin-x64"],
175
+ ["linux-x64", "hop-linux-x64"]
176
+ ]);
177
+ var releaseAssetName = (platform, arch) => RELEASE_ASSETS.get(`${platform}-${arch}`) ?? null;
178
+ var GITHUB_BACKEND_PREFIXES = ["github:", "ubi:", "aqua:"];
179
+ var NPM_BACKEND_PREFIX = "npm:";
180
+ var unsupported2 = (reason) => ({
181
+ kind: "unsupported",
182
+ reason
183
+ });
184
+ var miseChannel = (tool) => {
185
+ if (tool.startsWith(NPM_BACKEND_PREFIX)) {
186
+ return "npm";
187
+ }
188
+ return GITHUB_BACKEND_PREFIXES.some((prefix) => tool.startsWith(prefix)) ? "github" : null;
189
+ };
190
+ var insideVersionManager = (dir, marker) => unsupported2(`installed under a version manager's install dir (${dir}) ${marker}; update it with the tool that installed it`);
191
+ var detectUnmarkedToolDir = (dir, compiled) => {
192
+ if (isHopToolDirName(baseName(dir))) {
193
+ return unsupported2(`installed by mise, but its backend is unknown (no readable marker in ${dir}); run "mise upgrade" yourself`);
194
+ }
195
+ return compiled ? insideVersionManager(dir, "without a readable mise marker") : null;
196
+ };
197
+ var detectMise = ({ mise, compiled }) => {
198
+ if (mise === null) {
199
+ return null;
200
+ }
201
+ if (mise.backend === null) {
202
+ return detectUnmarkedToolDir(mise.dir, compiled);
203
+ }
204
+ const tool = withoutToolOptions(mise.backend);
205
+ if (!isHopBackend(tool)) {
206
+ return compiled ? insideVersionManager(mise.dir, `whose marker names "${tool}", not hop`) : null;
207
+ }
208
+ const channel = miseChannel(tool);
209
+ return channel === null ? unsupported2(`mise installed hop through "${tool}", which is neither a GitHub nor an npm backend; run "mise upgrade ${tool}" yourself`) : { kind: "mise", tool, channel };
210
+ };
211
+ var detectStandalone = (facts) => {
212
+ const { executablePath, platform, arch } = facts;
213
+ if (executablePath === null) {
214
+ return unsupported2("cannot tell where the hop binary is installed");
215
+ }
216
+ const managedBy = managerRootReason(executablePath);
217
+ if (managedBy !== null) {
218
+ return unsupported2(managedBy);
219
+ }
220
+ const assetName = releaseAssetName(platform, arch);
221
+ if (assetName === null) {
222
+ return unsupported2(`there is no prebuilt binary for ${platform}-${arch}; install with npm instead (npm i -g ${NPM_PACKAGE})`);
223
+ }
224
+ return {
225
+ kind: "standalone",
226
+ binaryPath: executablePath,
227
+ installDir: parentDir(executablePath),
228
+ assetName
229
+ };
230
+ };
231
+ var detectInstallMethod = (facts) => {
232
+ const mise = detectMise(facts);
233
+ if (mise !== null) {
234
+ return mise;
235
+ }
236
+ return facts.compiled ? detectStandalone(facts) : detectScriptInstall(facts.scriptPath ?? "", facts.npmGlobalPrefix);
237
+ };
238
+ var latestChannel = (method) => {
239
+ if (method.kind === "mise") {
240
+ return method.channel;
241
+ }
242
+ return method.kind === "standalone" ? "github" : "npm";
243
+ };
244
+ var upgradeCommand = (method) => {
245
+ if (method.kind === "mise") {
246
+ return ["mise", "upgrade", method.tool];
247
+ }
248
+ return method.kind === "npm" ? ["npm", "install", "-g", "--prefix", method.prefix, NPM_PACKAGE_LATEST] : ["bun", "add", "-g", NPM_PACKAGE_LATEST];
249
+ };
250
+ var miseDryRunCommand = (method) => [
251
+ "mise",
252
+ "upgrade",
253
+ "--dry-run-code",
254
+ method.tool
255
+ ];
256
+ var preferredProgramDir = (method) => method.kind === "npm" ? `${method.prefix}/bin` : null;
257
+
258
+ // src/commands/self-update-attempt.ts
259
+ var attempt = async (run) => {
260
+ try {
261
+ return { ok: true, value: await run() };
262
+ } catch (error) {
263
+ return { ok: false, error };
264
+ }
265
+ };
266
+
267
+ // src/commands/self-update-delegated.ts
268
+ var MISE_WOULD_NOT_UPGRADE = 0;
269
+ var runLogged = (port, term, executable, [, ...args]) => {
270
+ const command = [executable, ...args];
271
+ term.logStderr(`Running: ${command.join(" ")}`);
272
+ return attempt(() => port.runCommand(command));
273
+ };
274
+ var miseNothingToUpgradeWarning = ({ tool }, { current, latest }) => [
275
+ `mise has nothing to upgrade for ${tool}, so hop stays at ${current} although ${latest} is the latest release.`,
276
+ "Likely causes: the version is pinned in your global mise config, a minimum_release_age setting holds the release back,",
277
+ `or ${tool} is not in your global mise config (hop runs mise from your home directory, so a project's mise.toml does not count).`,
278
+ `Run "mise upgrade --dry-run ${tool}" from your home directory to see what mise decides.`
279
+ ].join(" ");
280
+ var runDelegated = async (port, term, method, base) => {
281
+ const argv = upgradeCommand(method);
282
+ const [program] = argv;
283
+ const command = argv.join(" ");
284
+ const executable = await port.resolveExecutable(program, preferredProgramDir(method));
285
+ if (executable === null) {
286
+ return fail(EXIT_GENERAL_ERROR, `Cannot find "${program}" on PATH. Run it yourself: ${command}`);
287
+ }
288
+ if (method.kind === "mise") {
289
+ const question = miseDryRunCommand(method);
290
+ const answer = await runLogged(port, term, executable, question);
291
+ if (!answer.ok) {
292
+ return fail(EXIT_GENERAL_ERROR, `Failed to run ${question.join(" ")}: ${messageOf(answer.error)}`);
293
+ }
294
+ if (answer.value === MISE_WOULD_NOT_UPGRADE) {
295
+ return ok({ data: base, warnings: [miseNothingToUpgradeWarning(method, base)] });
296
+ }
297
+ }
298
+ const run = await runLogged(port, term, executable, argv);
299
+ if (!run.ok) {
300
+ return fail(EXIT_GENERAL_ERROR, `Failed to run ${command}: ${messageOf(run.error)}`);
301
+ }
302
+ if (run.value !== 0) {
303
+ return fail(EXIT_GENERAL_ERROR, `${command} exited with code ${run.value}.`);
304
+ }
305
+ term.logStderr(`${command} finished; run "hop --version" to see which version you have now.`);
306
+ return ok({ data: { ...base, action: "delegated", command: [...argv] } });
307
+ };
308
+
309
+ // src/commands/self-update-standalone.ts
310
+ var checksumRejection = (port, assetName, checksumText, bytes) => {
311
+ const expected = parseChecksumFile(checksumText, assetName);
312
+ if (!expected.ok) {
313
+ return fail(EXIT_SAFE_REJECTION, `The checksum file ${assetName}.sha256 ${expected.reason}; refusing to install an unverified binary.`);
314
+ }
315
+ const actual = port.sha256Hex(bytes);
316
+ if (actual !== expected.digest) {
317
+ return fail(EXIT_SAFE_REJECTION, `Checksum mismatch for ${assetName} (expected ${expected.digest}, got ${actual}); refusing to install it. Nothing was changed.`);
318
+ }
319
+ return null;
320
+ };
321
+ 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)}`;
322
+ var replaceStandalone = async (port, term, method, base) => {
323
+ const { assetName, binaryPath } = method;
324
+ const checksumAsset = `${assetName}.sha256`;
325
+ const writable = await attempt(() => port.assertWritableDir(method.installDir));
326
+ if (!writable.ok) {
327
+ return fail(EXIT_GENERAL_ERROR, replaceFailureMessage(method, writable.error));
328
+ }
329
+ term.logStderr(`Downloading ${assetName} v${base.latest}...`);
330
+ const checksum = await attempt(() => port.downloadReleaseText(base.latest, checksumAsset));
331
+ if (!checksum.ok) {
332
+ return fail(EXIT_GENERAL_ERROR, `Failed to download ${checksumAsset}: ${messageOf(checksum.error)}`);
333
+ }
334
+ const binary = await attempt(() => port.downloadReleaseAsset(base.latest, assetName));
335
+ if (!binary.ok) {
336
+ return fail(EXIT_GENERAL_ERROR, `Failed to download ${assetName}: ${messageOf(binary.error)}`);
337
+ }
338
+ const rejection = checksumRejection(port, assetName, checksum.value, binary.value);
339
+ if (rejection !== null) {
340
+ return rejection;
341
+ }
342
+ term.logStderr(`Checksum verified. Replacing ${binaryPath}...`);
343
+ const replaced = await attempt(() => port.replaceExecutable(binaryPath, binary.value));
344
+ if (!replaced.ok) {
345
+ return fail(EXIT_GENERAL_ERROR, replaceFailureMessage(method, replaced.error));
346
+ }
347
+ term.logStderr(`Updated hop ${base.current} -> ${base.latest}.`);
348
+ return ok({ data: { ...base, action: "replaced" } });
349
+ };
350
+
351
+ // src/commands/self-update.ts
352
+ var fetchLatest = (port, method) => latestChannel(method) === "npm" ? port.latestNpmVersion() : port.latestGithubVersion();
353
+ var checkCommand = (method) => method.kind === "standalone" ? null : [...upgradeCommand(method)];
354
+ var describeCheck = (method, base) => {
355
+ const how = method.kind === "standalone" ? `replaces ${method.binaryPath}` : `runs: ${upgradeCommand(method).join(" ")}`;
356
+ return `Update available: ${base.current} -> ${base.latest}. "hop --update" ${how}.`;
357
+ };
358
+ var selfUpdate = async (port, term, options) => {
359
+ const facts = await attempt(() => port.installFacts());
360
+ if (!facts.ok) {
361
+ return fail(EXIT_GENERAL_ERROR, `Cannot update hop: could not tell how it was installed (${messageOf(facts.error)}).`);
362
+ }
363
+ const method = detectInstallMethod(facts.value);
364
+ if (method.kind === "unsupported") {
365
+ return fail(EXIT_GENERAL_ERROR, `Cannot update hop: ${method.reason}.`);
366
+ }
367
+ term.logStderr("Checking for the latest release...");
368
+ const release = await attempt(async () => {
369
+ const latest = await fetchLatest(port, method);
370
+ return {
371
+ latest,
372
+ updateAvailable: compareVersions(latest, options.currentVersion) > 0
373
+ };
374
+ });
375
+ if (!release.ok) {
376
+ return fail(EXIT_GENERAL_ERROR, `Failed to check for the latest release: ${messageOf(release.error)}`);
377
+ }
378
+ const base = {
379
+ current: options.currentVersion,
380
+ latest: release.value.latest,
381
+ updateAvailable: release.value.updateAvailable,
382
+ method: method.kind,
383
+ action: "none",
384
+ command: null
385
+ };
386
+ if (!base.updateAvailable) {
387
+ term.logStderr(`hop ${base.current} is up to date (latest release: ${base.latest}).`);
388
+ return ok({ data: base });
389
+ }
390
+ if (options.check) {
391
+ term.logStderr(describeCheck(method, base));
392
+ return ok({ data: { ...base, command: checkCommand(method) } });
393
+ }
394
+ return method.kind === "standalone" ? replaceStandalone(port, term, method, base) : runDelegated(port, term, method, base);
395
+ };
396
+
397
+ // src/infra/install-facts.ts
398
+ import { readFile, realpath } from "node:fs/promises";
399
+ import { join } from "node:path";
400
+ var MISE_BACKEND_TOML = ".mise.backend.toml";
401
+ var MISE_BACKEND_LEGACY = ".mise.backend";
402
+ var realpathOrNull = async (path) => {
403
+ try {
404
+ return await realpath(path);
405
+ } catch {
406
+ return null;
407
+ }
408
+ };
409
+ var realpathOrSelf = async (path) => await realpathOrNull(path) ?? path;
410
+ var readTextOrNull = async (path) => {
411
+ try {
412
+ return await readFile(path, "utf8");
413
+ } catch {
414
+ return null;
415
+ }
416
+ };
417
+ var readMiseBackend = async (dir) => {
418
+ const toml = await readTextOrNull(join(dir, MISE_BACKEND_TOML));
419
+ const fromToml = toml === null ? null : parseMiseBackendToml(toml);
420
+ if (fromToml !== null) {
421
+ return fromToml;
422
+ }
423
+ const legacy = await readTextOrNull(join(dir, MISE_BACKEND_LEGACY));
424
+ return legacy === null ? null : parseLegacyMiseBackend(legacy);
425
+ };
426
+ var findMiseTool = async (path) => {
427
+ const dir = path === null ? null : miseToolDir(path);
428
+ return dir === null ? null : { dir, backend: await readMiseBackend(dir) };
429
+ };
430
+ var verifiedNpmPrefix = async (scriptPath) => {
431
+ const prefix = scriptPath === null ? null : npmGlobalPrefixOf(scriptPath);
432
+ if (prefix === null) {
433
+ return null;
434
+ }
435
+ return await realpathOrNull(join(prefix, "bin", "hop")) === scriptPath ? prefix : null;
436
+ };
437
+ var readInstallFacts = async (source) => {
438
+ const executablePath = source.compiled ? await realpathOrSelf(source.execPath) : null;
439
+ const scriptPath = source.compiled || source.argv1 === undefined ? null : await realpathOrSelf(source.argv1);
440
+ return {
441
+ compiled: source.compiled,
442
+ executablePath,
443
+ scriptPath,
444
+ mise: await findMiseTool(executablePath ?? scriptPath),
445
+ npmGlobalPrefix: await verifiedNpmPrefix(scriptPath),
446
+ platform: source.platform,
447
+ arch: source.arch
448
+ };
449
+ };
450
+
451
+ // src/infra/package-manager.ts
452
+ import { spawn } from "node:child_process";
453
+ import { homedir } from "node:os";
454
+ import { basename } from "node:path";
455
+ var STDERR_FD = 2;
456
+ var resolveExecutable = (name, pathEnv, preferredDir = null) => firstExecutable(pathExecutableCandidates(name, pathEnv, preferredDir));
457
+ var runCommand = (argv) => new Promise((resolve, reject) => {
458
+ const [program, ...args] = argv;
459
+ const child = spawn(program, args, {
460
+ cwd: homedir(),
461
+ stdio: ["inherit", STDERR_FD, STDERR_FD]
462
+ });
463
+ child.once("error", reject);
464
+ child.once("close", (code, signal) => {
465
+ if (code === null) {
466
+ reject(new Error(`${basename(program)} was killed by signal ${signal ?? "unknown"}`));
467
+ return;
468
+ }
469
+ resolve(code);
470
+ });
471
+ });
472
+
473
+ // src/infra/release-binary.ts
474
+ import { createHash, randomBytes } from "node:crypto";
475
+ import { constants } from "node:fs";
476
+ import { access, open, realpath as realpath2, rename, rm } from "node:fs/promises";
477
+ import { basename as basename2, dirname, join as join2 } from "node:path";
478
+ var EXECUTABLE_MODE = 493;
479
+ var TEMP_SUFFIX_BYTES = 6;
480
+ var sha256Hex = (bytes) => createHash("sha256").update(bytes).digest("hex");
481
+ var removeQuietly = async (path) => {
482
+ try {
483
+ await rm(path, { force: true });
484
+ } catch {}
485
+ };
486
+ var writeDurably = async (path, bytes) => {
487
+ const handle = await open(path, "wx", EXECUTABLE_MODE);
488
+ try {
489
+ await handle.writeFile(bytes);
490
+ await handle.chmod(EXECUTABLE_MODE);
491
+ await handle.sync();
492
+ } finally {
493
+ await handle.close();
494
+ }
495
+ };
496
+ var replaceExecutable = async (path, bytes) => {
497
+ const target = await realpath2(path);
498
+ const suffix = randomBytes(TEMP_SUFFIX_BYTES).toString("hex");
499
+ const temp = join2(dirname(target), `.${basename2(target)}.update-${suffix}`);
500
+ try {
501
+ await writeDurably(temp, bytes);
502
+ await rename(temp, target);
503
+ } catch (error) {
504
+ await removeQuietly(temp);
505
+ throw error;
506
+ }
507
+ };
508
+ var assertWritableDir = (dir) => access(dir, constants.W_OK | constants.X_OK);
509
+
510
+ // node_modules/valibot/dist/index.mjs
511
+ var store$4;
512
+ var DEFAULT_CONFIG = {
513
+ lang: undefined,
514
+ message: undefined,
515
+ abortEarly: undefined,
516
+ abortPipeEarly: undefined
517
+ };
518
+ function getGlobalConfig(config$1) {
519
+ if (!config$1 && !store$4)
520
+ return DEFAULT_CONFIG;
521
+ return {
522
+ lang: config$1?.lang ?? store$4?.lang,
523
+ message: config$1?.message,
524
+ abortEarly: config$1?.abortEarly ?? store$4?.abortEarly,
525
+ abortPipeEarly: config$1?.abortPipeEarly ?? store$4?.abortPipeEarly
526
+ };
527
+ }
528
+ var store$3;
529
+ function getGlobalMessage(lang) {
530
+ return store$3?.get(lang);
531
+ }
532
+ var store$2;
533
+ function getSchemaMessage(lang) {
534
+ return store$2?.get(lang);
535
+ }
536
+ var store$1;
537
+ function getSpecificMessage(reference, lang) {
538
+ return store$1?.get(reference)?.get(lang);
539
+ }
540
+ function _stringify(input) {
541
+ const type = typeof input;
542
+ if (type === "string")
543
+ return `"${input}"`;
544
+ if (type === "number" || type === "bigint" || type === "boolean")
545
+ return `${input}`;
546
+ if (type === "object" || type === "function")
547
+ return (input && Object.getPrototypeOf(input)?.constructor?.name) ?? "null";
548
+ return type;
549
+ }
550
+ function _addIssue(context, label, dataset, config$1, other) {
551
+ const input = other && "input" in other ? other.input : dataset.value;
552
+ const expected = other?.expected ?? context.expects ?? null;
553
+ const received = other?.received ?? /* @__PURE__ */ _stringify(input);
554
+ const issue = {
555
+ kind: context.kind,
556
+ type: context.type,
557
+ input,
558
+ expected,
559
+ received,
560
+ message: `Invalid ${label}: ${expected ? `Expected ${expected} but r` : "R"}eceived ${received}`,
561
+ requirement: context.requirement,
562
+ path: other?.path,
563
+ issues: other?.issues,
564
+ lang: config$1.lang,
565
+ abortEarly: config$1.abortEarly,
566
+ abortPipeEarly: config$1.abortPipeEarly
567
+ };
568
+ const isSchema = context.kind === "schema";
569
+ 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);
570
+ if (message$1 !== undefined)
571
+ issue.message = typeof message$1 === "function" ? message$1(issue) : message$1;
572
+ if (isSchema)
573
+ dataset.typed = false;
574
+ if (dataset.issues)
575
+ dataset.issues.push(issue);
576
+ else
577
+ dataset.issues = [issue];
578
+ }
579
+ var _standardCache = /* @__PURE__ */ new WeakMap;
580
+ function _getStandardProps(context) {
581
+ let cached = _standardCache.get(context);
582
+ if (!cached) {
583
+ cached = {
584
+ version: 1,
585
+ vendor: "valibot",
586
+ validate(value$1) {
587
+ return context["~run"]({ value: value$1 }, /* @__PURE__ */ getGlobalConfig());
588
+ }
589
+ };
590
+ _standardCache.set(context, cached);
591
+ }
592
+ return cached;
593
+ }
594
+ function getFallback(schema, dataset, config$1) {
595
+ return typeof schema.fallback === "function" ? schema.fallback(dataset, config$1) : schema.fallback;
596
+ }
597
+ function getDefault(schema, dataset, config$1) {
598
+ return typeof schema.default === "function" ? schema.default(dataset, config$1) : schema.default;
599
+ }
600
+ function object(entries$1, message$1) {
601
+ return {
602
+ kind: "schema",
603
+ type: "object",
604
+ reference: object,
605
+ expects: "Object",
606
+ async: false,
607
+ entries: entries$1,
608
+ message: message$1,
609
+ get "~standard"() {
610
+ return /* @__PURE__ */ _getStandardProps(this);
611
+ },
612
+ "~run"(dataset, config$1) {
613
+ const input = dataset.value;
614
+ if (input && typeof input === "object") {
615
+ dataset.typed = true;
616
+ dataset.value = {};
617
+ for (const key in this.entries) {
618
+ const valueSchema = this.entries[key];
619
+ if (key in input || (valueSchema.type === "exact_optional" || valueSchema.type === "optional" || valueSchema.type === "nullish") && valueSchema.default !== undefined) {
620
+ const value$1 = key in input ? input[key] : /* @__PURE__ */ getDefault(valueSchema);
621
+ const valueDataset = valueSchema["~run"]({ value: value$1 }, config$1);
622
+ if (valueDataset.issues) {
623
+ const pathItem = {
624
+ type: "object",
625
+ origin: "value",
626
+ input,
627
+ key,
628
+ value: value$1
629
+ };
630
+ for (const issue of valueDataset.issues) {
631
+ if (issue.path)
632
+ issue.path.unshift(pathItem);
633
+ else
634
+ issue.path = [pathItem];
635
+ dataset.issues?.push(issue);
636
+ }
637
+ if (!dataset.issues)
638
+ dataset.issues = valueDataset.issues;
639
+ if (config$1.abortEarly) {
640
+ dataset.typed = false;
641
+ break;
642
+ }
643
+ }
644
+ if (!valueDataset.typed)
645
+ dataset.typed = false;
646
+ dataset.value[key] = valueDataset.value;
647
+ } else if (valueSchema.fallback !== undefined)
648
+ dataset.value[key] = /* @__PURE__ */ getFallback(valueSchema);
649
+ else if (valueSchema.type !== "exact_optional" && valueSchema.type !== "optional" && valueSchema.type !== "nullish") {
650
+ _addIssue(this, "key", dataset, config$1, {
651
+ input: undefined,
652
+ expected: `"${key}"`,
653
+ path: [{
654
+ type: "object",
655
+ origin: "key",
656
+ input,
657
+ key,
658
+ value: input[key]
659
+ }]
660
+ });
661
+ if (config$1.abortEarly)
662
+ break;
663
+ }
664
+ }
665
+ } else
666
+ _addIssue(this, "type", dataset, config$1);
667
+ return dataset;
668
+ }
669
+ };
670
+ }
671
+ function string(message$1) {
672
+ return {
673
+ kind: "schema",
674
+ type: "string",
675
+ reference: string,
676
+ expects: "string",
677
+ async: false,
678
+ message: message$1,
679
+ get "~standard"() {
680
+ return /* @__PURE__ */ _getStandardProps(this);
681
+ },
682
+ "~run"(dataset, config$1) {
683
+ if (typeof dataset.value === "string")
684
+ dataset.typed = true;
685
+ else
686
+ _addIssue(this, "type", dataset, config$1);
687
+ return dataset;
688
+ }
689
+ };
690
+ }
691
+ function safeParse(schema, input, config$1) {
692
+ const dataset = schema["~run"]({ value: input }, /* @__PURE__ */ getGlobalConfig(config$1));
693
+ return {
694
+ typed: dataset.typed,
695
+ success: !dataset.issues,
696
+ output: dataset.value,
697
+ issues: dataset.issues
698
+ };
699
+ }
700
+
701
+ // src/infra/capped-body.ts
702
+ var tooLarge = (maxBytes) => new Error(`the response is larger than ${maxBytes} bytes`);
703
+ var concatenate = (chunks, total) => {
704
+ const bytes = new Uint8Array(total);
705
+ let offset = 0;
706
+ for (const chunk of chunks) {
707
+ bytes.set(chunk, offset);
708
+ offset += chunk.length;
709
+ }
710
+ return bytes;
711
+ };
712
+ var readCapped = async (response, maxBytes) => {
713
+ const declared = Math.trunc(Number(response.headers.get("content-length") ?? ""));
714
+ if (declared > maxBytes) {
715
+ await response.body?.cancel();
716
+ throw tooLarge(maxBytes);
717
+ }
718
+ const chunks = [];
719
+ let received = 0;
720
+ for await (const chunk of response.body ?? []) {
721
+ received += chunk.length;
722
+ if (received > maxBytes) {
723
+ throw tooLarge(maxBytes);
724
+ }
725
+ chunks.push(chunk);
726
+ }
727
+ return concatenate(chunks, received);
728
+ };
729
+
730
+ // src/infra/release-http.ts
731
+ var METADATA_TIMEOUT_MS = 15000;
732
+ var ASSET_TIMEOUT_MS = 300000;
733
+ var METADATA_MAX_BYTES = 1048576;
734
+ var CHECKSUM_MAX_BYTES = 4096;
735
+ var BINARY_MAX_BYTES = 268435456;
736
+ var GITHUB_API_ORIGIN = "https://api.github.com/";
737
+ var HTTP_FORBIDDEN = 403;
738
+ var HTTP_TOO_MANY_REQUESTS = 429;
739
+ var GithubLatestSchema = object({ tag_name: string() });
740
+ var NpmLatestSchema = object({ version: string() });
741
+ var DECODER = new TextDecoder;
742
+ var send = async (fetcher, url, { headers, timeoutMs }) => {
743
+ try {
744
+ return await fetcher(url, {
745
+ headers,
746
+ redirect: "follow",
747
+ signal: AbortSignal.timeout(timeoutMs)
748
+ });
749
+ } catch (error) {
750
+ throw new Error(`GET ${url} failed: ${messageOf(error)}`, { cause: error });
751
+ }
752
+ };
753
+ var rateLimitHint = (url, response) => {
754
+ const refused = response.status === HTTP_FORBIDDEN || response.status === HTTP_TOO_MANY_REQUESTS;
755
+ const exhausted = refused && response.headers.get("x-ratelimit-remaining") === "0";
756
+ return exhausted && url.startsWith(GITHUB_API_ORIGIN) ? " (the unauthenticated GitHub API rate limit was hit; try again later)" : "";
757
+ };
758
+ var discardBody = async (response) => {
759
+ try {
760
+ await response.body?.cancel();
761
+ } catch {}
762
+ };
763
+ var get = async (fetcher, url, options) => {
764
+ const response = await send(fetcher, url, options);
765
+ if (!response.ok) {
766
+ await discardBody(response);
767
+ throw new Error(`GET ${url} failed: HTTP ${response.status}${rateLimitHint(url, response)}`);
768
+ }
769
+ if (response.url.startsWith("http://")) {
770
+ await discardBody(response);
771
+ throw new Error(`GET ${url} failed: redirected to ${response.url}, which is not HTTPS`);
772
+ }
773
+ return response;
774
+ };
775
+ var readBody = async (url, read) => {
776
+ try {
777
+ return await read();
778
+ } catch (error) {
779
+ throw new Error(`GET ${url} failed while reading the response: ${messageOf(error)}`, {
780
+ cause: error
781
+ });
782
+ }
783
+ };
784
+ var getBytes = async (fetcher, url, options) => {
785
+ const response = await get(fetcher, url, options);
786
+ return await readBody(url, () => readCapped(response, options.maxBytes));
787
+ };
788
+ var getJson = async (fetcher, url, schema, options) => {
789
+ const bytes = await getBytes(fetcher, url, options);
790
+ const body = await readBody(url, () => JSON.parse(DECODER.decode(bytes)));
791
+ const parsed = safeParse(schema, body);
792
+ if (!parsed.success) {
793
+ throw new Error(`GET ${url} returned an unexpected response`);
794
+ }
795
+ return parsed.output;
796
+ };
797
+ var createReleaseHttp = (userAgent, fetcher = fetch) => ({
798
+ async latestGithubVersion() {
799
+ const release = await getJson(fetcher, GITHUB_LATEST_RELEASE_URL, GithubLatestSchema, {
800
+ headers: {
801
+ Accept: "application/vnd.github+json",
802
+ "User-Agent": userAgent
803
+ },
804
+ timeoutMs: METADATA_TIMEOUT_MS,
805
+ maxBytes: METADATA_MAX_BYTES
806
+ });
807
+ return versionFromTag(release.tag_name);
808
+ },
809
+ async latestNpmVersion() {
810
+ const latest = await getJson(fetcher, NPM_LATEST_URL, NpmLatestSchema, {
811
+ headers: { Accept: "application/json", "User-Agent": userAgent },
812
+ timeoutMs: METADATA_TIMEOUT_MS,
813
+ maxBytes: METADATA_MAX_BYTES
814
+ });
815
+ return requireVersion(latest.version);
816
+ },
817
+ async downloadReleaseAsset(version, assetName) {
818
+ return await getBytes(fetcher, releaseAssetUrl(version, assetName), {
819
+ headers: { Accept: "application/octet-stream", "User-Agent": userAgent },
820
+ timeoutMs: ASSET_TIMEOUT_MS,
821
+ maxBytes: BINARY_MAX_BYTES
822
+ });
823
+ },
824
+ async downloadReleaseText(version, assetName) {
825
+ const bytes = await getBytes(fetcher, releaseAssetUrl(version, assetName), {
826
+ headers: { Accept: "text/plain", "User-Agent": userAgent },
827
+ timeoutMs: METADATA_TIMEOUT_MS,
828
+ maxBytes: CHECKSUM_MAX_BYTES
829
+ });
830
+ return DECODER.decode(bytes);
831
+ }
832
+ });
833
+
834
+ // src/infra/self-update.ts
835
+ var createSelfUpdatePort = ({
836
+ compiled,
837
+ version
838
+ }) => ({
839
+ installFacts: () => readInstallFacts({
840
+ compiled,
841
+ execPath: process.execPath,
842
+ argv1: process.argv[1],
843
+ platform: process.platform,
844
+ arch: process.arch
845
+ }),
846
+ ...createReleaseHttp(`hop/${version}`),
847
+ sha256Hex,
848
+ assertWritableDir,
849
+ replaceExecutable,
850
+ resolveExecutable: (name, preferredDir) => resolveExecutable(name, process.env.PATH, preferredDir),
851
+ runCommand
852
+ });
853
+
854
+ // src/cli-update.ts
855
+ var CHECK_FLAG = "--check";
856
+ var JSON_FLAG = "--json";
857
+ var UPDATE_FLAGS = new Set([CHECK_FLAG, JSON_FLAG]);
858
+ var parseUpdateArgs = (tokens) => {
859
+ const stray = tokens.filter((token) => !UPDATE_FLAGS.has(token));
860
+ if (stray.length > 0) {
861
+ return {
862
+ ok: false,
863
+ json: wantsJson(tokens),
864
+ message: `Unknown argument for --update: ${stray.join(" ")}. Usage: hop --update [--check] [--json]`
865
+ };
866
+ }
867
+ return { ok: true, check: tokens.includes(CHECK_FLAG), json: tokens.includes(JSON_FLAG) };
868
+ };
869
+ var runUpdate = async (rawArgs, term) => {
870
+ try {
871
+ const parsed = parseUpdateArgs(rawArgs.slice(1));
872
+ if (!parsed.ok) {
873
+ reportResult("update", fail(EXIT_USAGE_ERROR, parsed.message), parsed.json);
874
+ return;
875
+ }
876
+ const port = createSelfUpdatePort({
877
+ compiled: isRunningAsCompiledBinary(),
878
+ version: VERSION
879
+ });
880
+ const result = await selfUpdate(port, term, {
881
+ currentVersion: VERSION,
882
+ check: parsed.check
883
+ });
884
+ reportResult("update", result, parsed.json);
885
+ } catch (error) {
886
+ reportFatalError("update", wantsJson(rawArgs), error);
887
+ }
888
+ };
889
+ export {
890
+ runUpdate
891
+ };