fix(cli): a missing registry item names the closest ones, and a blocked browser says how to allow it

This commit is contained in:
Miguel Angel Simon Sierra
2026-10-30 01:54:40 -04:00
parent 2a9dd05983
commit 030a38ecd6
5 changed files with 34 additions and 3 deletions
@@ -278,6 +278,14 @@ describe("registry resolver", () => {
);
});
it("names the closest items when the requested one is not in the registry", async () => {
mockFetch();
const baseUrl = uniqueBaseUrl();
await expect(resolveItemWithDependencies("alpha-gamma-reveal", { baseUrl })).rejects.toThrow(
/Item "alpha-gamma-reveal" not found in registry\. Closest: alpha, gamma\. Available: /,
);
});
it("throws a clear cycle error for circular dependencies", async () => {
mockFetch({ dependencies: { alpha: ["gamma"], beta: ["alpha"], gamma: ["beta"] } });
const baseUrl = uniqueBaseUrl();
+6 -1
View File
@@ -7,6 +7,7 @@
import type { ItemType, RegistryItem, RegistryManifestEntry } from "@hyperframes/core";
import { fetchItemManifest, fetchRegistryManifest, DEFAULT_REGISTRY_URL } from "./remote.js";
import { runWithConcurrencyLimit } from "../utils/concurrency.js";
import { closestNames } from "../utils/didYouMean.js";
export interface ResolveOptions {
baseUrl?: string;
@@ -145,9 +146,13 @@ export async function resolveItemWithDependencies(
const entry = entries.find((e) => e.name === name);
if (!entry) {
const available = entries.map((e) => e.name).join(", ");
const close = closestNames(
name,
entries.map((e) => e.name),
);
throw new Error(
available.length > 0
? `Item "${name}" not found in registry. Available: ${available}`
? `Item "${name}" not found in registry.${close.length > 0 ? ` Closest: ${close.join(", ")}.` : ""} Available: ${available}`
: unreachableRegistryMessage(name, options.baseUrl),
);
}
+15
View File
@@ -29,6 +29,21 @@ export function closestMatch(input: string, candidates: Iterable<string>): strin
return best;
}
/** Up to `limit` candidates sharing a dash-separated word with `input`, most shared words first, then nearest spelling. */
export function closestNames(input: string, candidates: Iterable<string>, limit = 5): string[] {
const words = new Set(input.split("-"));
return [...candidates]
.map((name) => ({
name,
shared: name.split("-").filter((word) => words.has(word)).length,
distance: editDistance(input, name),
}))
.filter(({ shared }) => shared > 0)
.sort((a, b) => b.shared - a.shared || a.distance - b.distance || a.name.localeCompare(b.name))
.slice(0, limit)
.map(({ name }) => name);
}
const COMMAND_SYNONYMS: Record<string, string> = {
search: "catalog",
find: "catalog",
@@ -102,6 +102,8 @@ describe("explainChromeLaunchFailure for a browser Windows refuses to run", () =
expect(explained.message).toContain(executablePath);
expect(explained.message).toContain("error -4094");
expect(explained.message).toContain("browser ensure --force");
expect(explained.message).toContain("Application Control");
expect(explained.message).toContain("HYPERFRAMES_BROWSER_PATH");
expect(explained.cause).toBe(refused);
expect(existsSync(marker)).toBe(false);
});
@@ -109,8 +109,9 @@ export async function explainChromeLaunchFailure(
if (code === "UNKNOWN" && syscall === "spawn")
return new Error(
`Windows refused to start the browser${executablePath ? ` at ${executablePath}` : ""} (error ${errno ?? "unknown"}, spawn UNKNOWN). ` +
"Security software may be blocking it, or the computer may be out of memory. " +
"Close other apps and retry; if it persists, run `hyperframes browser ensure --force` to download it again.",
"Windows Application Control or security software may be blocking it, or the computer may be out of memory. " +
"Close other apps and retry. If a policy blocks the file, allow that path or set HYPERFRAMES_BROWSER_PATH to a Chrome or Edge your policy allows; " +
"if the file is damaged, run `hyperframes browser ensure --force` to download it again.",
{ cause: error },
);
if (!executablePath) return error;