fix(setup): persist detected Hopper launcher
This commit is contained in:
parent
88f5edc236
commit
b2866e4706
|
|
@ -48,7 +48,10 @@ export interface SetupHost {
|
|||
hopperPath(): Promise<string | undefined>;
|
||||
installHopper(): Promise<boolean>;
|
||||
detectedClients(): Promise<readonly SetupClient[]>;
|
||||
configureClient(client: SetupClient): Promise<ClientConfigurationResult>;
|
||||
configureClient(
|
||||
client: SetupClient,
|
||||
hopperPath: string,
|
||||
): Promise<ClientConfigurationResult>;
|
||||
installSkill(): Promise<"installed" | "unchanged" | "failed">;
|
||||
doctor(): Promise<Awaited<ReturnType<typeof runDoctor>>>;
|
||||
}
|
||||
|
|
@ -97,16 +100,22 @@ export const runSetup = async (
|
|||
);
|
||||
actions.push("installed_homebrew");
|
||||
}
|
||||
if ((await host.hopperPath()) === undefined) {
|
||||
let hopperPath = await host.hopperPath();
|
||||
if (hopperPath === undefined) {
|
||||
if (!yes) return fail("Re-run with --yes to install Hopper.");
|
||||
if (!(await host.installHopper()))
|
||||
return fail(
|
||||
"Hopper installation was interrupted; resolve the system prompt and re-run setup.",
|
||||
);
|
||||
actions.push("installed_hopper");
|
||||
hopperPath = await host.hopperPath();
|
||||
if (hopperPath === undefined)
|
||||
return fail(
|
||||
"Hopper installation completed but its launcher was not found.",
|
||||
);
|
||||
}
|
||||
for (const client of await host.detectedClients()) {
|
||||
const result = await host.configureClient(client);
|
||||
const result = await host.configureClient(client, hopperPath);
|
||||
clients[client.name] = result;
|
||||
if (result.status === "failed")
|
||||
return fail(
|
||||
|
|
@ -188,6 +197,7 @@ const detectJsonClients = async (
|
|||
/** Back up, atomically update, and semantically read back one JSON MCP configuration. */
|
||||
export const configureJsonClient = async (
|
||||
client: SetupClient,
|
||||
hopperPath?: string,
|
||||
): Promise<ClientConfigurationResult> => {
|
||||
let document: Record<string, unknown> = {};
|
||||
let original: string | undefined;
|
||||
|
|
@ -206,6 +216,9 @@ export const configureJsonClient = async (
|
|||
const desired = {
|
||||
command: "npx",
|
||||
args: ["-y", PRODUCT_IDENTITY.packageName, "mcp"],
|
||||
...(hopperPath === undefined
|
||||
? {}
|
||||
: { env: { HOPPER_LAUNCHER_PATH: hopperPath } }),
|
||||
};
|
||||
if (
|
||||
JSON.stringify(servers[PRODUCT_IDENTITY.mcpServerKey]) ===
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ class FakeSetupHost implements SetupHost {
|
|||
homebrewInstalls = 0;
|
||||
hopperInstalls = 0;
|
||||
configurations = 0;
|
||||
configuredHopperPaths: string[] = [];
|
||||
|
||||
macosVersion = (): Promise<string | undefined> =>
|
||||
Promise.resolve(this.version);
|
||||
|
|
@ -41,8 +42,10 @@ class FakeSetupHost implements SetupHost {
|
|||
Promise.resolve(this.clients);
|
||||
configureClient = (
|
||||
client: SetupClient,
|
||||
hopperPath: string,
|
||||
): Promise<ClientConfigurationResult> => {
|
||||
this.configurations += 1;
|
||||
this.configuredHopperPaths.push(hopperPath);
|
||||
return Promise.resolve(
|
||||
this.clientResults.get(client.name) ?? {
|
||||
status: "configured",
|
||||
|
|
@ -99,6 +102,7 @@ describe("setup workflow", () => {
|
|||
host.hopper = "/Applications/Manual Hopper";
|
||||
expect((await runSetup(true, host)).actions).toEqual(["installed_skill"]);
|
||||
expect(host.hopperInstalls).toBe(0);
|
||||
expect(host.configuredHopperPaths).toEqual([]);
|
||||
});
|
||||
|
||||
it.each(["homebrew", "hopper"] as const)(
|
||||
|
|
@ -127,6 +131,7 @@ describe("setup workflow", () => {
|
|||
const result = await runSetup(true, host);
|
||||
expect(Object.keys(result.clients)).toEqual(["claude", "cursor"]);
|
||||
expect(host.configurations).toBe(2);
|
||||
expect(host.configuredHopperPaths).toEqual(["/Hopper", "/Hopper"]);
|
||||
});
|
||||
|
||||
it.each(["backup", "write", "readback"] as const)(
|
||||
|
|
|
|||
|
|
@ -49,6 +49,28 @@ describe("JSON client configuration transaction", () => {
|
|||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("persists a custom Hopper launcher and remains idempotent", async () => {
|
||||
directory = await mkdtemp(join(tmpdir(), "rea-setup-"));
|
||||
const configPath = join(directory, "mcp.json");
|
||||
const hopperPath = "/Applications/Hopper v6.app/Contents/MacOS/hopper";
|
||||
const client = { name: "cursor", configPath };
|
||||
expect(await configureJsonClient(client, hopperPath)).toMatchObject({
|
||||
status: "configured",
|
||||
});
|
||||
expect(JSON.parse(await readFile(configPath, "utf8"))).toMatchObject({
|
||||
mcpServers: {
|
||||
rea: {
|
||||
command: "npx",
|
||||
args: ["-y", "@morluto/rea", "mcp"],
|
||||
env: { HOPPER_LAUNCHER_PATH: hopperPath },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(await configureJsonClient(client, hopperPath)).toEqual({
|
||||
status: "unchanged",
|
||||
});
|
||||
});
|
||||
|
||||
it("refuses malformed existing JSON without overwriting it", async () => {
|
||||
directory = await mkdtemp(join(tmpdir(), "rea-setup-"));
|
||||
const configPath = join(directory, "mcp.json");
|
||||
|
|
|
|||
Loading…
Reference in New Issue