mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(hopper): skip non-file launcher candidates
Require automatically discovered Hopper launchers to be executable regular files so POSIX directory search permission cannot hide a later valid application bundle. Preserve file symlinks, explicit caller configuration, and the shared candidate ordering. Cover canonical and renamed user bundles through the production filesystem predicate; respect existing canonical or renamed system installations in the isolated fixtures. Clarify launcher eligibility in the installation guide. Follow-up to #3085 and #3054 after their shared path discovery landed on main. Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
3 files changed
+38
-2
No files matched your search
@@ -585,6 +585,8 @@ system application, the canonical application installed by `rea setup` under
|
||||
diagnostic; to use a Homebrew-installed launcher for CLI/MCP startup, set
|
||||
`HOPPER_LAUNCHER_PATH` to the path reported by doctor. REA does not run Homebrew
|
||||
while loading runtime configuration.
|
||||
Automatic discovery accepts executable regular files, including file symlinks,
|
||||
and skips directories.
|
||||
On Linux, it prefers executable `/opt/hopper/bin/Hopper`, then executable
|
||||
`~/.local/share/rea/hopper/bin/Hopper`, followed by `/opt/hopper-v4/bin/hopper`
|
||||
and `/usr/bin/hopper`; if none exists, the former remains the diagnostic
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { accessSync, constants } from "node:fs";
|
||||
import { accessSync, constants, statSync } from "node:fs";
|
||||
|
||||
import type { ConfigurationError } from "../domain/configurationErrors.js";
|
||||
import { ok, type Result } from "../domain/result.js";
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
const executableAvailable = (path: string): boolean => {
|
||||
try {
|
||||
accessSync(path, constants.X_OK);
|
||||
return true;
|
||||
return statSync(path).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { accessSync, constants, statSync } from "node:fs";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { defaultHopperLauncherPath } from "../../../src/config/parseConfig.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
// A real system installation legitimately precedes this isolated user fixture.
|
||||
const installedSystemHopper = (home: string): boolean => {
|
||||
try {
|
||||
const launcher = defaultHopperLauncherPath("darwin", home);
|
||||
accessSync(launcher, constants.X_OK);
|
||||
return statSync(launcher).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
it.for(["Hopper Disassembler.app", "Hopper A.app"])(
|
||||
"skips a directory launcher in %s and selects a real file",
|
||||
async (name, context) => {
|
||||
const home = await createTestTempDirectory("rea-hopper-selection-");
|
||||
if (installedSystemHopper(home)) context.skip();
|
||||
const invalid = join(home, "Applications", name, "Contents/MacOS/hopper");
|
||||
const valid = join(home, "Applications/Hopper Z.app/Contents/MacOS/hopper");
|
||||
await mkdir(invalid, { recursive: true });
|
||||
await mkdir(dirname(valid), { recursive: true });
|
||||
await writeFile(valid, "#!/bin/sh\nexit 0\n", { mode: 0o700 });
|
||||
|
||||
expect(defaultHopperLauncherPath("darwin", home)).toBe(valid);
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user