mirror of
https://github.com/morluto/rea.git
synced 2026-11-04 05:46:17 +00:00
* fix(setup): resolve the configuration MSIX Claude Desktop reads On Windows, setup, doctor and uninstall always used %APPDATA%\Claude\claude_desktop_config.json. The MSIX build of Claude Desktop (package family Claude_pzs8sxrjxfjjc) virtualizes AppData writes and reads its package-private copy under %LOCALAPPDATA%\Packages\<family>\LocalCache\Roaming\Claude once it exists. Setup from an ordinary process therefore wrote a file the app never reads, and doctor reported that file healthy. Resolve the Claude Desktop location once, in supportedClients(): the private copy when it exists, else the AppData file the package still reads, else a new private copy. An unrecognized Claude_* publisher or unreadable package storage is reported instead of falling back to AppData. Compare one of the package's private files with its AppData path through realpath.native to learn whether the running package started this process. From that redirected view, or when no file can be compared, setup and uninstall refuse to change Claude Desktop's configuration, because the running app rewrites it from memory. From a redirected view they also refuse writes under %APPDATA% and %LOCALAPPDATA% for other clients and skills, which would land in the package's private storage. Setup refuses to register a launcher stored in any MSIX package's private storage, discloses which file the package reads, and tells the user to quit Claude Desktop fully on every platform. Configuration fails with target_changed if the package switched to its private copy after the plan was shown. Uninstall removes REA from both files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(setup): record the verified MSIX Claude Desktop registration On the Windows 11 host with the MSIX build 2.31226.1.0, setup from an ordinary terminal with the app quit wrote the package-private copy. After the app restarted, the entry persisted, doctor reported it aligned, and Claude Desktop's MCP log showed REA starting and answering tools/list. Uninstall against the real app remains unverified. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * style(setup): format MSIX review corrections * fix(setup): carry the admitted legacy path through uninstall removal clientRegistrationRemoval returned a removeLegacy boolean, and removeClient recomputed the legacy path separately, so TypeScript could not prove the path existed when removeLegacy was true and `tsc --noEmit` failed in Uninstall.ts. The decision now returns the admitted legacy path itself; preflight and removal consume that one value. The alias regression matched new-destination children with a "/" separator while building paths with path.join, so it failed on Windows. It now uses path.sep. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(setup): split MSIX Claude Desktop lifecycle cases under the function length limit The uninstall cases pushed the registration lifecycle describe past the 150-line lint limit. Group them separately, with the AppData registration case kept among the registration cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
212 lines
7.3 KiB
TypeScript
212 lines
7.3 KiB
TypeScript
import type { DoctorReport, DoctorScope, runDoctor } from "./Doctor.js";
|
|
import type { LinuxDistribution } from "./LinuxHopper.js";
|
|
import type { SkillDestination, SkillInstallResult } from "./SetupSkill.js";
|
|
import type { SetupClient } from "./SupportedClients.js";
|
|
import type {
|
|
SetupFailureCode,
|
|
SetupHopperInstallResult,
|
|
} from "./SetupInstallFailure.js";
|
|
|
|
/** Exact non-secret provider variables propagated into managed registrations. */
|
|
export type SetupProviderEnvironment = Readonly<Record<string, string>>;
|
|
|
|
/** Read-only host facts shared by the initial setup plan. */
|
|
export interface SetupInitialState {
|
|
readonly hopperPath?: string;
|
|
readonly providerEnvironment: SetupProviderEnvironment;
|
|
readonly doctor: DoctorReport;
|
|
}
|
|
|
|
/** Result of one backup/write/readback transaction. */
|
|
export type ClientConfigurationResult =
|
|
| {
|
|
readonly status: "unchanged" | "configured" | "skipped";
|
|
readonly backupPath?: string;
|
|
}
|
|
| {
|
|
readonly status: "failed";
|
|
readonly reason:
|
|
| "path"
|
|
| "target_changed"
|
|
| "backup"
|
|
| "write"
|
|
| "readback";
|
|
}
|
|
| {
|
|
readonly status: "failed";
|
|
readonly reason: "rollback";
|
|
readonly backupPath?: string;
|
|
readonly failure: {
|
|
readonly kind: "restore";
|
|
readonly path: string;
|
|
readonly detail: string;
|
|
};
|
|
};
|
|
|
|
/** Read-only state used to block unsafe setup plans before any mutation. */
|
|
export type ClientConfigurationInspection =
|
|
| { readonly status: "already_current" }
|
|
| {
|
|
readonly status: "create" | "update";
|
|
readonly backupPath?: string;
|
|
}
|
|
| {
|
|
readonly status: "invalid";
|
|
readonly remediation: string;
|
|
};
|
|
/** Effects required by the idempotent, non-blocking setup workflow. */
|
|
export interface SetupHost {
|
|
readonly platform: NodeJS.Platform;
|
|
readonly homeDirectory: string;
|
|
skillDestinations(clientIds: readonly string[]): readonly SkillDestination[];
|
|
readonly registrationCommand: readonly string[];
|
|
/** Why no client may be registered with `registrationCommand` from this host. */
|
|
readonly registrationCommandBlocker?: string;
|
|
readonly nodeVersion: string;
|
|
macosVersion(): Promise<string | undefined>;
|
|
linuxDistribution(): Promise<LinuxDistribution | undefined>;
|
|
initialSetupState(scope?: DoctorScope): Promise<SetupInitialState>;
|
|
installHopper(replaceExisting: boolean): Promise<SetupHopperInstallResult>;
|
|
/** Retry and report any cleanup resources still owned by this setup host. */
|
|
close?(): Promise<string | undefined>;
|
|
detectedClients(): Promise<readonly SetupClient[]>;
|
|
supportedClients(): Promise<readonly SetupClient[]>;
|
|
configureClient(
|
|
client: SetupClient,
|
|
providerEnvironment: SetupProviderEnvironment,
|
|
command: readonly string[],
|
|
): Promise<ClientConfigurationResult>;
|
|
clientNeedsConfigure(
|
|
client: SetupClient,
|
|
providerEnvironment: SetupProviderEnvironment,
|
|
command: readonly string[],
|
|
): Promise<boolean>;
|
|
inspectClientConfiguration(
|
|
client: SetupClient,
|
|
providerEnvironment: SetupProviderEnvironment,
|
|
command: readonly string[],
|
|
): Promise<ClientConfigurationInspection>;
|
|
skillNeedsInstall(clientIds: readonly string[]): Promise<boolean>;
|
|
installSkill(clientIds: readonly string[]): Promise<SkillInstallResult>;
|
|
doctor(scope?: DoctorScope): Promise<Awaited<ReturnType<typeof runDoctor>>>;
|
|
}
|
|
/** Structured setup outcome carrying remediation instead of prompting. */
|
|
export interface SetupResult {
|
|
readonly status:
|
|
| "planned"
|
|
| "cancelled"
|
|
| "needs_confirmation"
|
|
| "ready"
|
|
| "needs_human";
|
|
readonly plannedActions: readonly SetupAction[];
|
|
readonly appliedActions: readonly string[];
|
|
readonly clients: Readonly<Record<string, ClientConfigurationResult>>;
|
|
readonly doctor: SetupDoctorSummary;
|
|
/** Supported-client discovery state captured before setup mutations. */
|
|
readonly clientStates: readonly SetupClientState[];
|
|
readonly remediation?: string;
|
|
readonly code?: SetupFailureCode;
|
|
}
|
|
|
|
/** Whether the requested setup actions still need approval or human repair. */
|
|
export const isSetupFailure = (result: SetupResult): boolean => {
|
|
const status = result.status;
|
|
switch (status) {
|
|
case "planned":
|
|
case "cancelled":
|
|
case "ready":
|
|
return false;
|
|
case "needs_confirmation":
|
|
case "needs_human":
|
|
return true;
|
|
default: {
|
|
const exhaustive: never = status;
|
|
throw new TypeError(
|
|
`Unhandled setup result status: ${String(exhaustive)}`,
|
|
);
|
|
}
|
|
}
|
|
};
|
|
|
|
/** Relevant health and identity evidence for the selected setup scope. */
|
|
export interface SetupDoctorSummary {
|
|
readonly healthy: boolean;
|
|
readonly environment_healthy: boolean;
|
|
readonly scope: DoctorReport["scope"];
|
|
readonly hopperPath?: string;
|
|
readonly availableProviders: readonly string[];
|
|
readonly providerInspections?: DoctorReport["providerInspections"];
|
|
readonly identity?: Pick<
|
|
NonNullable<DoctorReport["identity"]>,
|
|
"skill" | "registrations"
|
|
>;
|
|
}
|
|
|
|
/** Read-only state for one supported client location. */
|
|
export interface SetupClientState {
|
|
readonly client: SetupClient;
|
|
readonly detected: boolean;
|
|
readonly configured: boolean;
|
|
readonly status: "configured" | "missing" | "needs_configuration" | "invalid";
|
|
}
|
|
|
|
/** One concrete setup mutation disclosed before approval. */
|
|
export interface SetupAction {
|
|
readonly id: string;
|
|
readonly kind: "install_hopper" | "configure_client" | "install_skill";
|
|
readonly label: string;
|
|
readonly target: string;
|
|
readonly detail: string;
|
|
readonly external: boolean;
|
|
readonly operation: "create" | "update" | "install";
|
|
readonly backupPath?: string;
|
|
readonly networkOrigins?: readonly string[];
|
|
readonly commands?: readonly string[];
|
|
readonly integrity?: string;
|
|
}
|
|
|
|
/** Explicit authorization supplied by an interactive or unattended CLI adapter. */
|
|
export interface SetupOptions {
|
|
readonly approved: boolean;
|
|
readonly installHopper: boolean;
|
|
readonly structured: boolean;
|
|
readonly proposeHopper?: boolean;
|
|
readonly clientIds?: readonly string[];
|
|
/** Skill roots to update independently of MCP registrations (maintenance). */
|
|
readonly skillClientIds?: readonly string[];
|
|
readonly installSkill?: boolean;
|
|
readonly readinessScope?: DoctorScope;
|
|
readonly dryRun?: boolean;
|
|
readonly allDetectedClients?: boolean;
|
|
readonly onProgress?: (event: SetupProgressEvent) => void;
|
|
}
|
|
|
|
/** One settled or active setup operation projected to the interactive adapter. */
|
|
export interface SetupProgressEvent {
|
|
readonly actionId: string;
|
|
readonly label: string;
|
|
readonly state: "started" | "completed" | "warning" | "failed";
|
|
readonly detail?: string;
|
|
}
|
|
|
|
/** Interactive selection and final consent returned by the CLI adapter. */
|
|
export interface SetupConfirmationDecision {
|
|
readonly approved: boolean;
|
|
readonly selectedActionIds: readonly string[];
|
|
readonly cancelled?: boolean;
|
|
}
|
|
|
|
/** Read-only context shown while selecting clients or reviewing exact actions. */
|
|
export interface SetupConfirmationContext {
|
|
readonly stage: "select" | "confirm";
|
|
readonly clientStates: readonly SetupClientState[];
|
|
readonly selectedClientIds: readonly string[];
|
|
readonly clientSelectionAllowed: boolean;
|
|
}
|
|
|
|
/** Adapter-owned selection and confirmation for the setup workflow. */
|
|
export type SetupConfirmation = (
|
|
actions: readonly SetupAction[],
|
|
context?: SetupConfirmationContext,
|
|
) => Promise<boolean | SetupConfirmationDecision>;
|