mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
fix(setup): ignore empty home directory selections
Skip empty home environment values while preserving nonempty caller selections and platform precedence. Setup, doctor, and uninstall now use the same fallback home instead of writing relative paths in the current directory. Validation: focused home tests and the public setup/doctor/uninstall lifecycle under Node 24.18. Package verification and pack dry run are included in the integrated gate. Fixes #2325
This commit is contained in:
@@ -1,17 +1,29 @@
|
||||
import { userInfo } from "node:os";
|
||||
|
||||
const firstNonempty = (...values: readonly (string | undefined)[]) =>
|
||||
values.find(
|
||||
(value): value is string => value !== undefined && value.length > 0,
|
||||
);
|
||||
|
||||
/** Resolve the caller's selected home before consulting the host account. */
|
||||
export const homeDirectoryFromEnvironment = (
|
||||
environment: Readonly<NodeJS.ProcessEnv>,
|
||||
platform: NodeJS.Platform,
|
||||
): string => {
|
||||
const windowsDriveHome =
|
||||
environment.HOMEDRIVE !== undefined &&
|
||||
environment.HOMEDRIVE.length > 0 &&
|
||||
environment.HOMEPATH !== undefined &&
|
||||
environment.HOMEPATH.length > 0
|
||||
? `${environment.HOMEDRIVE}${environment.HOMEPATH}`
|
||||
: undefined;
|
||||
const selected =
|
||||
platform === "win32"
|
||||
? (environment.USERPROFILE ??
|
||||
(environment.HOMEDRIVE !== undefined &&
|
||||
environment.HOMEPATH !== undefined
|
||||
? `${environment.HOMEDRIVE}${environment.HOMEPATH}`
|
||||
: environment.HOME))
|
||||
: (environment.HOME ?? environment.USERPROFILE);
|
||||
? firstNonempty(
|
||||
environment.USERPROFILE,
|
||||
windowsDriveHome,
|
||||
environment.HOME,
|
||||
)
|
||||
: firstNonempty(environment.HOME, environment.USERPROFILE);
|
||||
return selected ?? userInfo().homedir;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { userInfo } from "node:os";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { homeDirectoryFromEnvironment } from "./config/homeDirectory.js";
|
||||
|
||||
describe("home directory environment selection", () => {
|
||||
it("skips an empty HOME before selecting USERPROFILE on POSIX", () => {
|
||||
expect(
|
||||
homeDirectoryFromEnvironment(
|
||||
{ HOME: "", USERPROFILE: "relative/profile" },
|
||||
"linux",
|
||||
),
|
||||
).toBe("relative/profile");
|
||||
});
|
||||
|
||||
it("falls back to the account home when POSIX home variables are empty", () => {
|
||||
expect(
|
||||
homeDirectoryFromEnvironment({ HOME: "", USERPROFILE: "" }, "linux"),
|
||||
).toBe(userInfo().homedir);
|
||||
});
|
||||
|
||||
it("preserves a nonempty relative Windows profile ahead of drive variables", () => {
|
||||
expect(
|
||||
homeDirectoryFromEnvironment(
|
||||
{
|
||||
USERPROFILE: "relative/profile",
|
||||
HOMEDRIVE: "D:",
|
||||
HOMEPATH: "\\Users\\account",
|
||||
HOME: "other/home",
|
||||
},
|
||||
"win32",
|
||||
),
|
||||
).toBe("relative/profile");
|
||||
});
|
||||
|
||||
it("uses the Windows drive pair when USERPROFILE is empty", () => {
|
||||
expect(
|
||||
homeDirectoryFromEnvironment(
|
||||
{
|
||||
USERPROFILE: "",
|
||||
HOMEDRIVE: "D:",
|
||||
HOMEPATH: "\\Users\\account",
|
||||
HOME: "other/home",
|
||||
},
|
||||
"win32",
|
||||
),
|
||||
).toBe("D:\\Users\\account");
|
||||
});
|
||||
|
||||
it("falls back to the account home when all Windows home values are empty", () => {
|
||||
expect(
|
||||
homeDirectoryFromEnvironment(
|
||||
{
|
||||
USERPROFILE: "",
|
||||
HOMEDRIVE: "",
|
||||
HOMEPATH: "",
|
||||
HOME: "",
|
||||
},
|
||||
"win32",
|
||||
),
|
||||
).toBe(userInfo().homedir);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
import { access, mkdir, readFile } from "node:fs/promises";
|
||||
import { userInfo } from "node:os";
|
||||
import { isAbsolute, join, relative, sep } from "node:path";
|
||||
import { parse } from "jsonc-parser";
|
||||
|
||||
import { expect } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
cliTest(
|
||||
"setup dry run resolves empty home values without writing to the cwd",
|
||||
async ({ cli }) => {
|
||||
const directory = await createTestTempDirectory("rea-empty-home-dry-run-");
|
||||
const workingDirectory = join(directory, "cwd");
|
||||
await mkdir(workingDirectory);
|
||||
const hostHome = userInfo().homedir;
|
||||
const result = await cli.run({
|
||||
arguments: [
|
||||
"setup",
|
||||
"--client",
|
||||
"cursor",
|
||||
"--skill=false",
|
||||
"--dry-run",
|
||||
"--json",
|
||||
],
|
||||
cwd: workingDirectory,
|
||||
environment: {
|
||||
HOME: "",
|
||||
USERPROFILE: "",
|
||||
HOMEDRIVE: "",
|
||||
HOMEPATH: "",
|
||||
XDG_CONFIG_HOME: "",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.exitCode, result.stdout).toBe(0);
|
||||
const output = result.json as {
|
||||
plannedActions?: Array<{ kind?: unknown; target?: unknown }>;
|
||||
status?: unknown;
|
||||
};
|
||||
expect(["planned", "ready"]).toContain(output.status);
|
||||
const configTargets = (output.plannedActions ?? [])
|
||||
.filter((action) => action.kind === "configure_client")
|
||||
.map((action) => action.target);
|
||||
for (const target of configTargets) {
|
||||
expect(typeof target).toBe("string");
|
||||
if (typeof target !== "string") continue;
|
||||
expect(isAbsolute(target)).toBe(true);
|
||||
const relativeTarget = relative(hostHome, target);
|
||||
expect(
|
||||
relativeTarget === "" ||
|
||||
(!relativeTarget.startsWith(`..${sep}`) && relativeTarget !== ".."),
|
||||
).toBe(true);
|
||||
}
|
||||
expect(configTargets).not.toContain(
|
||||
join(workingDirectory, ".cursor", "mcp.json"),
|
||||
);
|
||||
await expect(
|
||||
access(join(workingDirectory, ".cursor", "mcp.json")),
|
||||
).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
cliTest.skipIf(process.platform === "win32")(
|
||||
"setup, doctor, and uninstall use the nonempty fallback home when HOME is empty",
|
||||
async ({ cli }) => {
|
||||
const directory = await createTestTempDirectory(
|
||||
"rea-empty-home-lifecycle-",
|
||||
);
|
||||
const home = join(directory, "fallback-home");
|
||||
const workingDirectory = join(directory, "cwd");
|
||||
await mkdir(home);
|
||||
await mkdir(workingDirectory);
|
||||
const environment = {
|
||||
HOME: "",
|
||||
USERPROFILE: home,
|
||||
XDG_CONFIG_HOME: join(home, ".config"),
|
||||
};
|
||||
const run = (arguments_: readonly string[]) =>
|
||||
cli.run({
|
||||
arguments: arguments_,
|
||||
cwd: workingDirectory,
|
||||
environment,
|
||||
});
|
||||
|
||||
const setup = await run(["setup", "--client", "cursor", "--yes", "--json"]);
|
||||
expect(setup.exitCode, setup.stderr).toBe(0);
|
||||
expect(setup.json).toMatchObject({ status: "ready" });
|
||||
await access(join(home, ".cursor", "mcp.json"));
|
||||
await access(
|
||||
join(home, ".agents", "skills", "reverse-engineer-anything", "SKILL.md"),
|
||||
);
|
||||
await expect(
|
||||
access(join(workingDirectory, ".cursor", "mcp.json")),
|
||||
).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
await expect(
|
||||
access(join(workingDirectory, ".agents", "skills")),
|
||||
).rejects.toMatchObject({ code: "ENOENT" });
|
||||
|
||||
const doctor = await run([
|
||||
"doctor",
|
||||
"--client",
|
||||
"cursor",
|
||||
"--skill",
|
||||
"--json",
|
||||
]);
|
||||
expect(doctor.exitCode, doctor.stdout).toBe(0);
|
||||
expect(doctor.json).toMatchObject({ healthy: true });
|
||||
|
||||
const uninstall = await run(["uninstall", "--json"]);
|
||||
expect(uninstall.exitCode, uninstall.stderr).toBe(0);
|
||||
expect(uninstall.json).toMatchObject({ status: "complete" });
|
||||
const remainingConfig = parse(
|
||||
await readFile(join(home, ".cursor", "mcp.json"), "utf8"),
|
||||
);
|
||||
expect(remainingConfig).not.toHaveProperty("mcpServers.rea");
|
||||
await expect(
|
||||
access(
|
||||
join(
|
||||
home,
|
||||
".agents",
|
||||
"skills",
|
||||
"reverse-engineer-anything",
|
||||
"SKILL.md",
|
||||
),
|
||||
),
|
||||
).rejects.toMatchObject({ code: "ENOENT" });
|
||||
await expect(
|
||||
access(join(workingDirectory, ".cursor", "mcp.json")),
|
||||
).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user