mirror of
https://github.com/morluto/rea.git
synced 2026-11-09 08:17:02 +00:00
* fix(setup): refuse Hopper on macOS below the release's minimum Hopper setup planning accepted any macOS 12+ host as supported, and the installer never compared the host with the selected release. The current publisher-signed Hopper 6.6.1-demo requires macOS 12.4 (feed deployment_target and the app's LSMinimumSystemVersion), so setup on 12.0-12.3 installed an app the host cannot run. The installer now takes the minimum from the release that owns it: the feed's deployment_target before the package download, and the signed app's LSMinimumSystemVersion (decoded in-process after signature verification) before copying. A host below either fails with the new incompatible_host_version code and an installer detail naming both versions; an incomparable app value is a bundle failure. Planning keeps REA's own macOS 12 floor but reports support as unknown, since the release minimum is read only after approval, and the plan discloses the check. Info.plist decoding is shared with app-bundle resolution. Fixes #8502 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(setup): compare the macOS Hopper plan detail with its owning disclosure The planning test froze an explanatory sentence from the setup plan. The macOS install detail now lives in macHopperInstallDisclosure beside the commands and origins the installer owns, the plan uses it, and the test compares the plan with that constant while keeping its independent support-state assertions. An installer test also pins that the signed app's requirement is read only after both signature checks pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(setup): compare macOS Hopper refusal details with their owners The installer's refusal details were template literals inside installMacHopper, so tests copied whole sentences. The installer now formats them through exported macHopperHostVersionDetail and macHopperIncomparableMinimumDetail. Tests compare each diagnostic and the setup remediation with those owners and SetupInstallFailure's recovery, and separately assert the URL, versions, setting name, failure code and request order. The Info.plist type refusal is checked by error type, key and path instead of a sentence fragment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
284 lines
10 KiB
TypeScript
284 lines
10 KiB
TypeScript
import { HOPPER_PUBLISHER } from "../domain/hopperPublisher.js";
|
|
import {
|
|
describeHopperRequestFailure,
|
|
hopperRequestRecovery,
|
|
type HopperRequestFailure,
|
|
} from "./HopperDownload.js";
|
|
import { HOPPER_LINUX_PRIMARY_FINGERPRINT } from "./HopperReleaseFeed.js";
|
|
import type { LinuxPackageInstallFailure } from "./LinuxHopper.js";
|
|
|
|
export type SetupFailureCode =
|
|
| "unsupported_host"
|
|
| "incompatible_host_version"
|
|
| "download_failed"
|
|
| "integrity_mismatch"
|
|
| "authorization_or_package_manager_failed"
|
|
| "launcher_missing"
|
|
| "runtime_dependency_unavailable"
|
|
| "unsupported_hopper_build"
|
|
| "setup_cancelled"
|
|
| "destination_exists"
|
|
| "destination_inspection_failed"
|
|
| "archive_write_failed"
|
|
| "launcher_inspection_failed"
|
|
| "mount_failed"
|
|
| "bundle_invalid"
|
|
| "publisher_verification_failed"
|
|
| "copy_failed"
|
|
| "skill_install_failed"
|
|
| "cleanup_failed";
|
|
|
|
export type SetupHopperInstallResult =
|
|
| {
|
|
readonly status: "installed";
|
|
readonly launcherPath: string;
|
|
readonly cleanupFailure?: string;
|
|
readonly warning?: string;
|
|
}
|
|
| {
|
|
readonly status: "failed";
|
|
readonly code: SetupFailureCode;
|
|
readonly remediation: string;
|
|
/** The vendor request whose observed transport failure produced `code`. */
|
|
readonly download?: HopperRequestFailure;
|
|
};
|
|
|
|
export type SetupHopperInstallFailure = Extract<
|
|
SetupHopperInstallResult,
|
|
{ readonly status: "failed" }
|
|
>;
|
|
|
|
export type HopperInstallFailureReason =
|
|
| "unsupported_host"
|
|
| "incompatible_host_version"
|
|
| "release_metadata"
|
|
| "download"
|
|
| "integrity"
|
|
| "authorization_or_package_manager"
|
|
| "launcher_missing"
|
|
| "runtime_dependencies"
|
|
| "unsupported_hopper_build"
|
|
| "cancelled"
|
|
| "destination_exists"
|
|
| "destination_inspection"
|
|
| "archive_write"
|
|
| "launcher_inspection"
|
|
| "mount"
|
|
| "bundle"
|
|
| "publisher_verification_failed"
|
|
| "verification_unavailable"
|
|
| "cleanup_failed"
|
|
| "copy";
|
|
|
|
const normalizePackageOutput = (output: string): string =>
|
|
output
|
|
.replace(/\u001b(?:\[[0-9;?]*[ -/]*[@-~]|\([0-9A-Za-z]|[@-Z\\^_])/gu, "")
|
|
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, "")
|
|
.replace(/\s+/gu, " ")
|
|
.trim();
|
|
|
|
const packageOutputExcerpt = (
|
|
failure: LinuxPackageInstallFailure,
|
|
limit = 400,
|
|
): string => {
|
|
const output = [failure.stderr, failure.stdout]
|
|
.map(normalizePackageOutput)
|
|
.filter((text) => text !== "")
|
|
.join(" ");
|
|
const suffix = failure.outputTruncated ? " (output truncated)" : "";
|
|
const bodyLimit = limit - suffix.length;
|
|
if (output.length <= bodyLimit) return `${output}${suffix}`;
|
|
const half = Math.floor((bodyLimit - 3) / 2);
|
|
const head = output
|
|
.slice(0, half)
|
|
.replace(/\s+\S*$/u, "")
|
|
.trimEnd();
|
|
const tail = output
|
|
.slice(-half)
|
|
.replace(/^\S*\s+/u, "")
|
|
.trimStart();
|
|
return `${head} … ${tail}${suffix}`;
|
|
};
|
|
|
|
const packageManagerRemediation = (
|
|
failure: LinuxPackageInstallFailure | undefined,
|
|
): string => {
|
|
if (failure === undefined)
|
|
return "Allow the system package-manager operation or install the planned Hopper package and Linux runtime dependencies, then rerun rea setup.";
|
|
const status =
|
|
failure.exitCode === null ? "no exit status" : `exit ${failure.exitCode}`;
|
|
const excerpt = packageOutputExcerpt(failure);
|
|
const observed =
|
|
excerpt === "" ? "no output was captured." : `its output: ${excerpt}`;
|
|
return `The system package-manager command failed (${failure.command}; ${status}); ${observed} Resolve the reported problem, grant the authorization the command needs, or install the planned Hopper package and Linux runtime dependencies manually, then rerun rea setup.`;
|
|
};
|
|
|
|
/**
|
|
* Map provider installer failures to stable setup codes and safe recovery. A
|
|
* failed vendor request replaces the stage's generic advice with its observed
|
|
* URL and outcome, and stays structured for programmatic callers.
|
|
*/
|
|
export const setupInstallFailure = (
|
|
reason: HopperInstallFailureReason,
|
|
context: {
|
|
readonly packageFailure?: LinuxPackageInstallFailure | undefined;
|
|
readonly platform?: NodeJS.Platform;
|
|
readonly download?: HopperRequestFailure | undefined;
|
|
} = {},
|
|
): SetupHopperInstallFailure => {
|
|
const failure = stageFailure(
|
|
reason,
|
|
context.packageFailure,
|
|
context.platform ?? "darwin",
|
|
);
|
|
return context.download === undefined
|
|
? failure
|
|
: {
|
|
...failure,
|
|
remediation: `${describeHopperRequestFailure(context.download)} ${hopperRequestRecovery(context.download)}`,
|
|
download: context.download,
|
|
};
|
|
};
|
|
|
|
const stageFailure = (
|
|
reason: HopperInstallFailureReason,
|
|
packageFailure: LinuxPackageInstallFailure | undefined,
|
|
platform: NodeJS.Platform,
|
|
): SetupHopperInstallFailure => {
|
|
switch (reason) {
|
|
case "unsupported_host":
|
|
return {
|
|
status: "failed",
|
|
code: "unsupported_host",
|
|
remediation: "Use a supported host, then rerun rea setup.",
|
|
};
|
|
case "incompatible_host_version":
|
|
return {
|
|
status: "failed",
|
|
code: "incompatible_host_version",
|
|
remediation:
|
|
"The current Hopper release requires a newer macOS version than this Mac runs, so REA did not install it. Update macOS to at least the version the installer detail names, or install a Hopper release that supports this macOS version yourself and set HOPPER_LAUNCHER_PATH to its launcher, then rerun rea setup.",
|
|
};
|
|
case "integrity":
|
|
return {
|
|
status: "failed",
|
|
code: "integrity_mismatch",
|
|
remediation:
|
|
"The Hopper download failed integrity verification. Retry setup; if it repeats, update REA before installing.",
|
|
};
|
|
case "authorization_or_package_manager":
|
|
return {
|
|
status: "failed",
|
|
code: "authorization_or_package_manager_failed",
|
|
remediation: packageManagerRemediation(packageFailure),
|
|
};
|
|
case "launcher_missing":
|
|
return {
|
|
status: "failed",
|
|
code: "launcher_missing",
|
|
remediation:
|
|
"The package operation completed but Hopper is not runnable. Run rea doctor and repair the reported launcher issue.",
|
|
};
|
|
case "runtime_dependencies":
|
|
return {
|
|
status: "failed",
|
|
code: "runtime_dependency_unavailable",
|
|
remediation:
|
|
"Hopper's required runtime could not be verified. Inspect rea doctor --provider hopper; on a supported system use rea setup --install-hopper to repair its dependencies.",
|
|
};
|
|
case "unsupported_hopper_build":
|
|
return {
|
|
status: "failed",
|
|
code: "unsupported_hopper_build",
|
|
remediation:
|
|
"The installed Hopper launcher does not match the authenticated vendor package. Check HOPPER_LAUNCHER_PATH and reinstall the official Hopper demo through rea setup.",
|
|
};
|
|
case "cancelled":
|
|
return {
|
|
status: "failed",
|
|
code: "setup_cancelled",
|
|
remediation:
|
|
"Setup was cancelled. Run rea doctor to inspect the current state before retrying.",
|
|
};
|
|
case "destination_inspection":
|
|
return {
|
|
status: "failed",
|
|
code: "destination_inspection_failed",
|
|
remediation:
|
|
"REA could not inspect the Hopper installation destination. Check access to the reported path before retrying; an existing installation has not been established.",
|
|
};
|
|
case "archive_write":
|
|
return {
|
|
status: "failed",
|
|
code: "archive_write_failed",
|
|
remediation:
|
|
"The Hopper download completed, but REA could not stage its local archive. Check the reported path, permissions and free space, then retry setup.",
|
|
};
|
|
case "launcher_inspection":
|
|
return {
|
|
status: "failed",
|
|
code: "launcher_inspection_failed",
|
|
remediation:
|
|
"REA could not verify the installed Hopper launcher. Repair the reported inspection failure before retrying; launcher absence has not been established.",
|
|
};
|
|
case "destination_exists":
|
|
return {
|
|
status: "failed",
|
|
code: "destination_exists",
|
|
remediation:
|
|
"The Hopper installation destination already exists and was not replaced. Reuse the existing installation or move it aside, then rerun rea setup.",
|
|
};
|
|
case "mount":
|
|
return {
|
|
status: "failed",
|
|
code: "mount_failed",
|
|
remediation:
|
|
"The verified Hopper disk image could not be mounted. Eject any stale Hopper image, check local disk access, then rerun rea setup.",
|
|
};
|
|
case "bundle":
|
|
return {
|
|
status: "failed",
|
|
code: "bundle_invalid",
|
|
remediation:
|
|
"The mounted package did not contain the expected Hopper application bundle. Retry setup; if it repeats, update REA before installing.",
|
|
};
|
|
case "publisher_verification_failed":
|
|
return {
|
|
status: "failed",
|
|
code: "publisher_verification_failed",
|
|
remediation:
|
|
platform === "linux"
|
|
? `The Hopper package signature did not verify under the pinned primary PGP fingerprint ${HOPPER_LINUX_PRIMARY_FINGERPRINT}; the package manager was not run. Download the official package and retry setup.`
|
|
: `The mounted Hopper application is not signed by the publisher REA pins (${HOPPER_PUBLISHER.bundleIdentifier}, Apple team ${HOPPER_PUBLISHER.teamIdentifier}); nothing was copied or launched. Install Hopper yourself and set HOPPER_LAUNCHER_PATH to its launcher, then rerun rea setup.`,
|
|
};
|
|
case "verification_unavailable":
|
|
return {
|
|
status: "failed",
|
|
code: "publisher_verification_failed",
|
|
remediation:
|
|
"REA could not authenticate or retain the signed Hopper package. Check vendor access, native package inspection tools and caller-owned cache permissions, then retry setup.",
|
|
};
|
|
case "cleanup_failed":
|
|
return {
|
|
status: "failed",
|
|
code: "cleanup_failed",
|
|
remediation:
|
|
"Hopper publisher verification retained temporary cleanup ownership. Retry cleanup before running setup again.",
|
|
};
|
|
case "copy":
|
|
return {
|
|
status: "failed",
|
|
code: "copy_failed",
|
|
remediation:
|
|
"The verified Hopper application could not be copied to the installation destination. Check destination permissions and free space, then rerun rea setup.",
|
|
};
|
|
default:
|
|
return {
|
|
status: "failed",
|
|
code: "download_failed",
|
|
remediation:
|
|
"REA could not download or unpack the official Hopper package. Check network and filesystem access, then retry setup.",
|
|
};
|
|
}
|
|
};
|