fix(shared): stop ancestor discovery at worktree root

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-03-26 12:15:47 +09:00
parent a391f44420
commit 83819a15d3
2 changed files with 92 additions and 25 deletions

View File

@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, it } from "bun:test"
import { mkdirSync, rmSync } from "node:fs"
import { mkdirSync, realpathSync, rmSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import {
@@ -11,6 +11,10 @@ import {
const TEST_DIR = join(tmpdir(), `project-discovery-dirs-${Date.now()}`)
function canonicalPath(path: string): string {
return realpathSync(path)
}
describe("project-discovery-dirs", () => {
beforeEach(() => {
mkdirSync(TEST_DIR, { recursive: true })
@@ -33,9 +37,9 @@ describe("project-discovery-dirs", () => {
// then
expect(directories).toEqual([
join(projectDir, ".opencode", "skills"),
join(projectDir, ".opencode", "skill"),
join(TEST_DIR, ".opencode", "skills"),
canonicalPath(join(projectDir, ".opencode", "skills")),
canonicalPath(join(projectDir, ".opencode", "skill")),
canonicalPath(join(TEST_DIR, ".opencode", "skills")),
])
})
@@ -51,8 +55,8 @@ describe("project-discovery-dirs", () => {
// then
expect(directories).toEqual([
join(projectDir, ".opencode", "commands"),
join(TEST_DIR, ".opencode", "command"),
canonicalPath(join(projectDir, ".opencode", "commands")),
canonicalPath(join(TEST_DIR, ".opencode", "command")),
])
})
@@ -68,7 +72,21 @@ describe("project-discovery-dirs", () => {
const agentsDirectories = findProjectAgentsSkillDirs(childDir)
// then
expect(claudeDirectories).toEqual([join(projectDir, ".claude", "skills")])
expect(agentsDirectories).toEqual([join(TEST_DIR, ".agents", "skills")])
expect(claudeDirectories).toEqual([canonicalPath(join(projectDir, ".claude", "skills"))])
expect(agentsDirectories).toEqual([canonicalPath(join(TEST_DIR, ".agents", "skills"))])
})
it("#given a stop directory #when finding ancestor dirs #then it does not scan beyond the stop boundary", () => {
// given
const projectDir = join(TEST_DIR, "project")
const childDir = join(projectDir, "apps", "cli")
mkdirSync(join(projectDir, ".opencode", "skills"), { recursive: true })
mkdirSync(join(TEST_DIR, ".opencode", "skills"), { recursive: true })
// when
const directories = findProjectOpencodeSkillDirs(childDir, projectDir)
// then
expect(directories).toEqual([canonicalPath(join(projectDir, ".opencode", "skills"))])
})
})

View File

@@ -1,13 +1,29 @@
import { existsSync } from "node:fs"
import { execFileSync } from "node:child_process"
import { existsSync, realpathSync } from "node:fs"
import { dirname, join, resolve } from "node:path"
function normalizePath(path: string): string {
const resolvedPath = resolve(path)
if (!existsSync(resolvedPath)) {
return resolvedPath
}
try {
return realpathSync(resolvedPath)
} catch {
return resolvedPath
}
}
function findAncestorDirectories(
startDirectory: string,
targetPaths: ReadonlyArray<ReadonlyArray<string>>,
stopDirectory?: string,
): string[] {
const directories: string[] = []
const seen = new Set<string>()
let currentDirectory = resolve(startDirectory)
let currentDirectory = normalizePath(startDirectory)
const resolvedStopDirectory = stopDirectory ? normalizePath(stopDirectory) : undefined
while (true) {
for (const targetPath of targetPaths) {
@@ -20,33 +36,66 @@ function findAncestorDirectories(
directories.push(candidateDirectory)
}
if (resolvedStopDirectory === currentDirectory) {
return directories
}
const parentDirectory = dirname(currentDirectory)
if (parentDirectory === currentDirectory) {
return directories
}
currentDirectory = parentDirectory
currentDirectory = normalizePath(parentDirectory)
}
}
export function findProjectClaudeSkillDirs(startDirectory: string): string[] {
return findAncestorDirectories(startDirectory, [[".claude", "skills"]])
function detectWorktreePath(directory: string): string | undefined {
try {
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
cwd: directory,
encoding: "utf-8",
timeout: 5000,
stdio: ["pipe", "pipe", "pipe"],
}).trim()
} catch {
return undefined
}
}
export function findProjectAgentsSkillDirs(startDirectory: string): string[] {
return findAncestorDirectories(startDirectory, [[".agents", "skills"]])
export function findProjectClaudeSkillDirs(startDirectory: string, stopDirectory?: string): string[] {
return findAncestorDirectories(
startDirectory,
[[".claude", "skills"]],
stopDirectory ?? detectWorktreePath(startDirectory),
)
}
export function findProjectOpencodeSkillDirs(startDirectory: string): string[] {
return findAncestorDirectories(startDirectory, [
[".opencode", "skills"],
[".opencode", "skill"],
])
export function findProjectAgentsSkillDirs(startDirectory: string, stopDirectory?: string): string[] {
return findAncestorDirectories(
startDirectory,
[[".agents", "skills"]],
stopDirectory ?? detectWorktreePath(startDirectory),
)
}
export function findProjectOpencodeCommandDirs(startDirectory: string): string[] {
return findAncestorDirectories(startDirectory, [
[".opencode", "commands"],
[".opencode", "command"],
])
export function findProjectOpencodeSkillDirs(startDirectory: string, stopDirectory?: string): string[] {
return findAncestorDirectories(
startDirectory,
[
[".opencode", "skills"],
[".opencode", "skill"],
],
stopDirectory ?? detectWorktreePath(startDirectory),
)
}
export function findProjectOpencodeCommandDirs(startDirectory: string, stopDirectory?: string): string[] {
return findAncestorDirectories(
startDirectory,
[
[".opencode", "commands"],
[".opencode", "command"],
],
stopDirectory ?? detectWorktreePath(startDirectory),
)
}