diff --git a/packages/cli/src/registry/resolver.test.ts b/packages/cli/src/registry/resolver.test.ts index 910a0c065d..8ee896e337 100644 --- a/packages/cli/src/registry/resolver.test.ts +++ b/packages/cli/src/registry/resolver.test.ts @@ -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(); diff --git a/packages/cli/src/registry/resolver.ts b/packages/cli/src/registry/resolver.ts index f22e40909e..fe59b3a0b4 100644 --- a/packages/cli/src/registry/resolver.ts +++ b/packages/cli/src/registry/resolver.ts @@ -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), ); } diff --git a/packages/cli/src/utils/didYouMean.ts b/packages/cli/src/utils/didYouMean.ts index 3fdd7f4fde..1ca4b82302 100644 --- a/packages/cli/src/utils/didYouMean.ts +++ b/packages/cli/src/utils/didYouMean.ts @@ -29,6 +29,21 @@ export function closestMatch(input: string, candidates: Iterable): 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, 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 = { search: "catalog", find: "catalog", diff --git a/packages/engine/src/services/chromeStartup.test.ts b/packages/engine/src/services/chromeStartup.test.ts index be3cc7589f..1025f92fed 100644 --- a/packages/engine/src/services/chromeStartup.test.ts +++ b/packages/engine/src/services/chromeStartup.test.ts @@ -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); }); diff --git a/packages/engine/src/services/chromeStartup.ts b/packages/engine/src/services/chromeStartup.ts index ffc7985734..d24ec8bba7 100644 --- a/packages/engine/src/services/chromeStartup.ts +++ b/packages/engine/src/services/chromeStartup.ts @@ -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;