mirror of
https://github.com/morluto/rea
synced 2026-10-28 06:46:47 -04:00
fix(setup): preserve configuration directory requirements
This commit is contained in:
@@ -74,10 +74,17 @@ const canonicalRegularConfigPath = async (
|
||||
): Promise<string | undefined> => {
|
||||
if (fileSystem.realpath === undefined) return requestedPath;
|
||||
let ancestor = absoluteCallerPath(requestedPath);
|
||||
const directoryRequired =
|
||||
process.platform !== "win32" && ancestor.endsWith("/");
|
||||
const missing: string[] = [];
|
||||
while (true) {
|
||||
try {
|
||||
return join(await fileSystem.realpath(ancestor), ...missing);
|
||||
const canonical = join(await fileSystem.realpath(ancestor), ...missing);
|
||||
// Reconstructing missing children must retain the caller's directory
|
||||
// requirement; otherwise a missing `config.json/` becomes a writable file.
|
||||
return directoryRequired && !canonical.endsWith("/")
|
||||
? `${canonical}/`
|
||||
: canonical;
|
||||
} catch (cause: unknown) {
|
||||
if (!isAbsentPath(cause) || dirname(ancestor) === ancestor)
|
||||
return requestedPath;
|
||||
|
||||
@@ -84,7 +84,10 @@ export class ClientConfigurationPublication {
|
||||
selection?: ClientConfigurationSelection,
|
||||
): Promise<ClientConfigurationPublication> {
|
||||
const parent = await realpath(dirname(path));
|
||||
const destination = join(parent, basename(path));
|
||||
// Retain the transaction owner's directory requirement through final-file
|
||||
// publication rather than allowing basename/join to select a regular file.
|
||||
const destination =
|
||||
join(parent, basename(path)) + (path.endsWith(sep) ? sep : "");
|
||||
const input = await selectedConfiguration(destination, previous);
|
||||
if (selection !== undefined) {
|
||||
await assertRequestedConfigurationPath(path, selection.requestedPath);
|
||||
|
||||
@@ -40,7 +40,7 @@ for (const [version, keyStyle] of [
|
||||
"timestamp: 2026-10-14T00:00:00.123456+08:00\n";
|
||||
const original =
|
||||
settings +
|
||||
"mcp_servers:\n rea:\n command: rea-old\n args: [mcp]\n env:\n" +
|
||||
"mcp_servers:\n rea:\n command: rea\n args: [mcp]\n env:\n" +
|
||||
Object.entries(environment)
|
||||
.map(
|
||||
([key, value]) =>
|
||||
@@ -114,7 +114,7 @@ cliTest(
|
||||
"# Caller configuration",
|
||||
"mcp_servers:",
|
||||
" rea:",
|
||||
" command: rea-old",
|
||||
" command: rea",
|
||||
" args: [mcp]",
|
||||
" env:",
|
||||
" CALLER_SETTING: keep exactly",
|
||||
@@ -173,7 +173,7 @@ cliTest(
|
||||
},
|
||||
});
|
||||
expect(parse(configured)).not.toMatchObject({
|
||||
mcp_servers: { rea: { command: "rea-old" } },
|
||||
mcp_servers: { rea: { command: "rea" } },
|
||||
});
|
||||
expect(configured).toContain("# Curated for this client.");
|
||||
expect(await readFile(`${config}.rea.backup`, "utf8")).toBe(original);
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
symlink,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { join, relative } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parse as parseToml } from "smol-toml";
|
||||
|
||||
@@ -25,6 +25,7 @@ import { supportedClients } from "../../../src/application/SupportedClients.js";
|
||||
import { readClientRegistrationStatuses } from "../../../src/application/ClientRegistrationStatus.js";
|
||||
import { failedConfigurationMessage } from "../../../src/application/SetupClients.js";
|
||||
import { PRODUCT_IDENTITY } from "../../../src/identity.js";
|
||||
import { CURRENT_ENTRYPOINT_PATH } from "../../../src/process/CurrentEntrypointPath.js";
|
||||
|
||||
const pinnedPackage = PRODUCT_IDENTITY.registrationPackageSpecifier;
|
||||
|
||||
@@ -34,9 +35,9 @@ it.skipIf(process.platform === "win32")(
|
||||
const home = await createTestTempDirectory(
|
||||
"rea-config-equivalent-launcher-",
|
||||
);
|
||||
const real = join(home, "rea-current");
|
||||
const real = CURRENT_ENTRYPOINT_PATH;
|
||||
if (real === undefined) throw new Error("missing current entrypoint");
|
||||
const alias = join(home, "rea-alias");
|
||||
await writeFile(real, "current launcher\n");
|
||||
await symlink(real, alias);
|
||||
await mkdir(join(home, ".cursor"));
|
||||
const configPath = join(home, ".cursor", "mcp.json");
|
||||
@@ -76,9 +77,9 @@ it.skipIf(process.platform === "win32")(
|
||||
const home = await createTestTempDirectory(
|
||||
"rea-config-timeout-launcher-alias-",
|
||||
);
|
||||
const real = join(home, "rea-current");
|
||||
const real = CURRENT_ENTRYPOINT_PATH;
|
||||
if (real === undefined) throw new Error("missing current entrypoint");
|
||||
const alias = join(home, "rea-alias");
|
||||
await writeFile(real, "current launcher\n");
|
||||
await symlink(real, alias);
|
||||
await mkdir(join(home, ".codex"));
|
||||
const configPath = join(home, ".codex", "config.toml");
|
||||
@@ -157,6 +158,37 @@ it.skipIf(process.platform === "win32").each([false, true])(
|
||||
},
|
||||
);
|
||||
|
||||
it
|
||||
.skipIf(process.platform === "win32")
|
||||
.each(
|
||||
["/", "/."].flatMap((suffix) =>
|
||||
[false, true].map((relativePath) => ({ suffix, relativePath })),
|
||||
),
|
||||
)(
|
||||
"rejects a missing configuration with directory suffix $suffix and relative=$relativePath",
|
||||
async ({ suffix, relativePath }) => {
|
||||
const root = await createTestTempDirectory("rea-config-directory-suffix-");
|
||||
const path = join(root, "config.json");
|
||||
const requested = `${relativePath ? relative(process.cwd(), path) : path}${suffix}`;
|
||||
expect(await resolveClientConfigTransactionPath(requested)).toBe(
|
||||
`${path}/`,
|
||||
);
|
||||
expect(
|
||||
await configureClientConfiguration({
|
||||
name: "cursor",
|
||||
format: "json",
|
||||
configPath: requested,
|
||||
}),
|
||||
).toMatchObject({ status: "failed" });
|
||||
const selected = await lstat(path).catch((cause: unknown) => {
|
||||
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
|
||||
return undefined;
|
||||
throw cause;
|
||||
});
|
||||
expect(selected?.isFile() ?? false).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
describe("JSON client configuration transaction: writing configuration through safe paths", () => {
|
||||
it.skipIf(typeof process.getuid !== "function")(
|
||||
"rejects a symlink target not owned by the current user",
|
||||
|
||||
Reference in New Issue
Block a user