fix(FN-XXX): harden windows path handling
This commit is contained in:
38
packages/core/src/__tests__/pi-extensions-format.test.ts
Normal file
38
packages/core/src/__tests__/pi-extensions-format.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formatPiExtensionSource } from "../pi-extensions.js";
|
||||
|
||||
describe("formatPiExtensionSource", () => {
|
||||
it("formats project-relative extension paths", () => {
|
||||
expect(
|
||||
formatPiExtensionSource(
|
||||
"fusion-project",
|
||||
"/repo/.fusion/extensions/tooling/index.ts",
|
||||
"/repo",
|
||||
"/Users/alice",
|
||||
),
|
||||
).toBe("fusion-project: .fusion/extensions/tooling/index.ts");
|
||||
});
|
||||
|
||||
it("formats home-relative Windows extension paths", () => {
|
||||
expect(
|
||||
formatPiExtensionSource(
|
||||
"fusion-global",
|
||||
"C:\\Users\\alice\\.fusion\\agent\\extensions\\tooling\\index.ts",
|
||||
"C:\\repo",
|
||||
"C:\\Users\\alice",
|
||||
),
|
||||
).toBe("fusion-global: ~/.fusion/agent/extensions/tooling/index.ts");
|
||||
});
|
||||
|
||||
it("does not treat prefix-matching sibling paths as inside the project or home", () => {
|
||||
const projectLookalike = "C:\\repo-two\\.fusion\\extensions\\tooling\\index.ts";
|
||||
const homeLookalike = "C:\\Users\\alice-dev\\.fusion\\agent\\extensions\\tooling\\index.ts";
|
||||
|
||||
expect(
|
||||
formatPiExtensionSource("fusion-project", projectLookalike, "C:\\repo", "C:\\Users\\alice"),
|
||||
).toBe(`fusion-project: ${projectLookalike}`);
|
||||
expect(
|
||||
formatPiExtensionSource("fusion-global", homeLookalike, "C:\\repo", "C:\\Users\\alice"),
|
||||
).toBe(`fusion-global: ${homeLookalike}`);
|
||||
});
|
||||
});
|
||||
@@ -8,10 +8,12 @@ const TEMP_HOME_PREFIX = "fn-test-home-";
|
||||
describe("test isolation setup", () => {
|
||||
it("process.env.HOME is overridden to a temp directory", () => {
|
||||
const home = process.env.HOME;
|
||||
const userProfile = process.env.USERPROFILE;
|
||||
|
||||
expect(home).toBeDefined();
|
||||
expect(home).toContain(tmpdir());
|
||||
expect(home).toContain(TEMP_HOME_PREFIX);
|
||||
expect(userProfile).toBe(home);
|
||||
});
|
||||
|
||||
it("homedir() resolves to the temp HOME", () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { basename, join, relative, resolve, sep } from "node:path";
|
||||
import { basename, isAbsolute, join, relative, resolve, sep, win32 } from "node:path";
|
||||
|
||||
const FUSION_DISABLED_EXTENSIONS_KEY = "fusionDisabledExtensions";
|
||||
|
||||
@@ -276,13 +276,28 @@ export function reconcileClaudeCliPaths(
|
||||
return filtered;
|
||||
}
|
||||
|
||||
function getDisplayPathWithinRoot(root: string, targetPath: string): string | null {
|
||||
const usesWindowsPaths = /^[A-Za-z]:[\\/]/.test(root) || /^[A-Za-z]:[\\/]/.test(targetPath) || root.includes("\\") || targetPath.includes("\\");
|
||||
const pathApi = usesWindowsPaths ? win32 : { relative, isAbsolute, sep };
|
||||
const rel = pathApi.relative(root, targetPath);
|
||||
if (rel === "") {
|
||||
return "";
|
||||
}
|
||||
if (!rel || rel === ".." || rel.startsWith(`..${pathApi.sep}`) || pathApi.isAbsolute(rel)) {
|
||||
return null;
|
||||
}
|
||||
return rel.split(pathApi.sep).join("/");
|
||||
}
|
||||
|
||||
export function formatPiExtensionSource(source: PiExtensionSource, extensionPath: string, cwd: string, home?: string): string {
|
||||
const homeDir = getHomeDir(home);
|
||||
const projectRoot = resolvePiExtensionProjectRoot(cwd);
|
||||
const relativePath = extensionPath.startsWith(homeDir)
|
||||
? `~${extensionPath.slice(homeDir.length)}`
|
||||
: extensionPath.startsWith(projectRoot)
|
||||
? relative(projectRoot, extensionPath).split(sep).join("/")
|
||||
const relativeToHome = getDisplayPathWithinRoot(homeDir, extensionPath);
|
||||
const relativeToProject = getDisplayPathWithinRoot(projectRoot, extensionPath);
|
||||
const relativePath = relativeToHome !== null
|
||||
? relativeToHome.length > 0 ? `~/${relativeToHome}` : "~"
|
||||
: relativeToProject !== null
|
||||
? relativeToProject || "."
|
||||
: extensionPath;
|
||||
return `${source}: ${relativePath}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user