feat(FN-4904): complete Step 1 — promote worktree root resolver
Fusion-Task-Id: FN-4904 Fusion-Task-Lineage: 5da52c8c-0ed6-4e3a-920e-aa1b56d0b719
This commit is contained in:
committed by
gsxdsm
parent
3d729c7a50
commit
87d259b366
28
packages/core/src/__tests__/pi-extensions.test.ts
Normal file
28
packages/core/src/__tests__/pi-extensions.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getProjectRootFromWorktree } from "../pi-extensions.js";
|
||||
|
||||
describe("getProjectRootFromWorktree", () => {
|
||||
it("detects POSIX worktree paths", () => {
|
||||
expect(getProjectRootFromWorktree("/repo/.worktrees/fn-001")).toBe("/repo");
|
||||
expect(getProjectRootFromWorktree("/repo/.worktrees/fn-001/src/file.ts")).toBe("/repo");
|
||||
});
|
||||
|
||||
it("detects Windows worktree paths", () => {
|
||||
expect(getProjectRootFromWorktree("C:\\repo\\.worktrees\\fn-001")).toBe("C:\\repo");
|
||||
expect(getProjectRootFromWorktree("C:\\repo\\.worktrees\\fn-001\\src\\file.ts")).toBe("C:\\repo");
|
||||
});
|
||||
|
||||
it("supports configured candidate worktrees dir paths", () => {
|
||||
expect(
|
||||
getProjectRootFromWorktree("/tmp/.fn-worktrees/repo/fn-001/src", {
|
||||
worktreesDirCandidates: ["/tmp/.fn-worktrees/repo"],
|
||||
}),
|
||||
).toBe("/tmp/.fn-worktrees");
|
||||
|
||||
expect(
|
||||
getProjectRootFromWorktree("/tmp/repo.worktrees/fn-001", {
|
||||
worktreesDirCandidates: ["/tmp/repo.worktrees"],
|
||||
}),
|
||||
).toBe("/tmp");
|
||||
});
|
||||
});
|
||||
@@ -194,7 +194,7 @@ export {
|
||||
MasterKeyPermissionError,
|
||||
MasterKeyCorruptError,
|
||||
} from "./master-key.js";
|
||||
export { discoverPiExtensions, formatPiExtensionSource, getEnabledPiExtensionPaths, getFusionAgentDir, getFusionAgentSettingsPath, getLegacyPiAgentDir, getPiExtensionDiscoveryDirs, reconcileClaudeCliPaths, reconcileDroidCliPaths, resolvePiExtensionProjectRoot, updatePiExtensionDisabledIds } from "./pi-extensions.js";
|
||||
export { discoverPiExtensions, formatPiExtensionSource, getEnabledPiExtensionPaths, getFusionAgentDir, getFusionAgentSettingsPath, getLegacyPiAgentDir, getPiExtensionDiscoveryDirs, getProjectRootFromWorktree, reconcileClaudeCliPaths, reconcileDroidCliPaths, resolvePiExtensionProjectRoot, updatePiExtensionDisabledIds } from "./pi-extensions.js";
|
||||
export type { PiExtensionEntry, PiExtensionSettings, PiExtensionSource } from "./pi-extensions.js";
|
||||
export { canTransition, getValidTransitions, resolveDependencyOrder } from "./board.js";
|
||||
export { computeBlockerFanoutMap, BLOCKER_ESCALATION_COLUMNS, isStaleBlockedByBlocker } from "./blocker-fanout.js";
|
||||
|
||||
@@ -36,7 +36,39 @@ export function getFusionAgentSettingsPath(home?: string): string {
|
||||
return join(getFusionAgentDir(home), "settings.json");
|
||||
}
|
||||
|
||||
export function getProjectRootFromWorktree(
|
||||
cwd: string,
|
||||
opts?: { worktreesDirCandidates?: string[] },
|
||||
): string | null {
|
||||
const legacyMatch = cwd.match(/^(.+?)[\\/]\.worktrees[\\/][^\\/]+(?:[\\/]|$)/);
|
||||
if (legacyMatch) {
|
||||
return legacyMatch[1]!;
|
||||
}
|
||||
|
||||
for (const candidate of opts?.worktreesDirCandidates ?? []) {
|
||||
const normalizedCandidate = resolve(candidate);
|
||||
const normalizedCwd = resolve(cwd);
|
||||
const rel = relative(normalizedCandidate, normalizedCwd);
|
||||
if (rel !== "" && !rel.startsWith("..") && !isAbsolute(rel)) {
|
||||
const firstSegment = rel.split(/[\\/]/).filter(Boolean)[0];
|
||||
if (firstSegment) {
|
||||
const parent = normalizedCandidate.split(/[\\/]/).slice(0, -1).join("/");
|
||||
if (parent) {
|
||||
return parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolvePiExtensionProjectRoot(cwd: string): string {
|
||||
const worktreeProjectRoot = getProjectRootFromWorktree(cwd);
|
||||
if (worktreeProjectRoot && existsSync(join(worktreeProjectRoot, ".fusion"))) {
|
||||
return worktreeProjectRoot;
|
||||
}
|
||||
|
||||
let current = resolve(cwd);
|
||||
while (true) {
|
||||
if (existsSync(join(current, ".fusion"))) {
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
type AgentSession,
|
||||
type ToolDefinition,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
import { getEnabledPiExtensionPaths, getFusionAgentDir, getLegacyPiAgentDir, reconcileClaudeCliPaths, reconcileDroidCliPaths, resolvePiExtensionProjectRoot } from "@fusion/core";
|
||||
import { getEnabledPiExtensionPaths, getFusionAgentDir, getLegacyPiAgentDir, getProjectRootFromWorktree, reconcileClaudeCliPaths, reconcileDroidCliPaths, resolvePiExtensionProjectRoot } from "@fusion/core";
|
||||
import type {
|
||||
AgentPermissionPolicyActionCategory,
|
||||
PermanentAgentActionCategory,
|
||||
@@ -1225,41 +1225,7 @@ async function registerExtensionProviders(cwd: string, modelRegistry: ModelRegis
|
||||
|
||||
// ── Worktree Path Boundary Helpers ──────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Detect if a path is a task worktree under `.worktrees/`.
|
||||
* Returns the project root if the path is a worktree, otherwise null.
|
||||
*
|
||||
* Examples:
|
||||
* `/project/.worktrees/fn-001` → `/project`
|
||||
* `/project/.worktrees/fn-001/src/file.ts` → `/project`
|
||||
* `/project` → null (not a worktree)
|
||||
*/
|
||||
export function getProjectRootFromWorktree(
|
||||
cwd: string,
|
||||
opts?: { worktreesDirCandidates?: string[] },
|
||||
): string | null {
|
||||
const legacyMatch = cwd.match(/^(.+?)[\\/]\.worktrees[\\/][^\\/]+(?:[\\/]|$)/);
|
||||
if (legacyMatch) {
|
||||
return legacyMatch[1]!;
|
||||
}
|
||||
|
||||
for (const candidate of opts?.worktreesDirCandidates ?? []) {
|
||||
const normalizedCandidate = resolve(candidate);
|
||||
const normalizedCwd = resolve(cwd);
|
||||
const rel = relative(normalizedCandidate, normalizedCwd);
|
||||
if (rel !== "" && !rel.startsWith("..") && !isAbsolute(rel)) {
|
||||
const firstSegment = rel.split(/[\\/]/).filter(Boolean)[0];
|
||||
if (firstSegment) {
|
||||
const parent = normalizedCandidate.split(/[\\/]/).slice(0, -1).join("/");
|
||||
if (parent) {
|
||||
return parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
export { getProjectRootFromWorktree };
|
||||
|
||||
async function isRegisteredGitWorktree(projectRoot: string, worktreePath: string): Promise<boolean> {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user