fix(git): use stable executable for worktree discovery
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
import { execFileSync } from "node:child_process"
|
|
||||||
import { promises as fs } from "node:fs"
|
import { promises as fs } from "node:fs"
|
||||||
import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test"
|
import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test"
|
||||||
import { mkdirSync, rmSync, writeFileSync } from "node:fs"
|
import { mkdirSync, rmSync, writeFileSync } from "node:fs"
|
||||||
import { tmpdir } from "node:os"
|
import { tmpdir } from "node:os"
|
||||||
import { join } from "node:path"
|
import { join } from "node:path"
|
||||||
|
import { spawnSync } from "../../shared/bun-spawn-shim"
|
||||||
|
import { resolveGitExecutable } from "../../shared/git-executable"
|
||||||
import * as loader from "./loader"
|
import * as loader from "./loader"
|
||||||
|
|
||||||
const TEST_DIR = join(tmpdir(), `claude-code-command-loader-${Date.now()}`)
|
const TEST_DIR = join(tmpdir(), `claude-code-command-loader-${Date.now()}`)
|
||||||
@@ -16,6 +17,17 @@ function writeCommand(directory: string, name: string, description: string): voi
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function runGit(args: string[], cwd: string): void {
|
||||||
|
const result = spawnSync([resolveGitExecutable(), ...args], {
|
||||||
|
cwd,
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
})
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
throw new Error(new TextDecoder().decode(result.stderr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
describe("claude-code command loader", () => {
|
describe("claude-code command loader", () => {
|
||||||
let originalClaudeConfigDir: string | undefined
|
let originalClaudeConfigDir: string | undefined
|
||||||
let originalOpencodeConfigDir: string | undefined
|
let originalOpencodeConfigDir: string | undefined
|
||||||
@@ -128,10 +140,7 @@ describe("claude-code command loader", () => {
|
|||||||
const repositoryDir = join(TEST_DIR, "repo")
|
const repositoryDir = join(TEST_DIR, "repo")
|
||||||
const nestedDirectory = join(repositoryDir, "packages", "app", "src")
|
const nestedDirectory = join(repositoryDir, "packages", "app", "src")
|
||||||
mkdirSync(nestedDirectory, { recursive: true })
|
mkdirSync(nestedDirectory, { recursive: true })
|
||||||
execFileSync("git", ["init"], {
|
runGit(["init"], repositoryDir)
|
||||||
cwd: repositoryDir,
|
|
||||||
stdio: ["ignore", "ignore", "ignore"],
|
|
||||||
})
|
|
||||||
writeCommand(join(repositoryDir, ".opencode", "commands", "deploy"), "staging", "Deploy staging")
|
writeCommand(join(repositoryDir, ".opencode", "commands", "deploy"), "staging", "Deploy staging")
|
||||||
writeCommand(join(repositoryDir, ".opencode", "command"), "release", "Release command")
|
writeCommand(join(repositoryDir, ".opencode", "command"), "release", "Release command")
|
||||||
writeCommand(join(TEST_DIR, ".opencode", "commands"), "outside", "Outside command")
|
writeCommand(join(TEST_DIR, ".opencode", "commands"), "outside", "Outside command")
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { execFileSync } from "node:child_process"
|
|
||||||
import { afterEach, beforeEach, describe, expect, it } from "bun:test"
|
import { afterEach, beforeEach, describe, expect, it } from "bun:test"
|
||||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
|
import { existsSync, mkdtempSync, mkdirSync, realpathSync, rmSync, writeFileSync } from "node:fs"
|
||||||
import { tmpdir } from "node:os"
|
import { tmpdir } from "node:os"
|
||||||
import { join } from "node:path"
|
import { join } from "node:path"
|
||||||
|
import { spawnSync } from "../../shared/bun-spawn-shim"
|
||||||
|
import { resolveGitExecutable } from "../../shared/git-executable"
|
||||||
|
import { detectWorktreePath } from "../../shared/project-discovery-dirs"
|
||||||
import {
|
import {
|
||||||
discoverOpencodeProjectSkills,
|
discoverOpencodeProjectSkills,
|
||||||
discoverProjectAgentsSkills,
|
discoverProjectAgentsSkills,
|
||||||
@@ -17,6 +19,21 @@ function writeSkill(directory: string, name: string, description: string): void
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function runGit(args: string[], cwd: string): void {
|
||||||
|
const result = spawnSync([resolveGitExecutable(), ...args], {
|
||||||
|
cwd,
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
})
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
throw new Error(new TextDecoder().decode(result.stderr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function canonicalPath(path: string): string {
|
||||||
|
return realpathSync(path)
|
||||||
|
}
|
||||||
|
|
||||||
describe("project skill discovery", () => {
|
describe("project skill discovery", () => {
|
||||||
let tempDir = ""
|
let tempDir = ""
|
||||||
|
|
||||||
@@ -34,10 +51,38 @@ describe("project skill discovery", () => {
|
|||||||
const nestedDirectory = join(repositoryDir, "packages", "app", "src")
|
const nestedDirectory = join(repositoryDir, "packages", "app", "src")
|
||||||
|
|
||||||
mkdirSync(nestedDirectory, { recursive: true })
|
mkdirSync(nestedDirectory, { recursive: true })
|
||||||
execFileSync("git", ["init"], {
|
runGit(["init"], repositoryDir)
|
||||||
cwd: repositoryDir,
|
expect(existsSync(join(repositoryDir, ".git"))).toBe(true)
|
||||||
stdio: ["ignore", "ignore", "ignore"],
|
const gitExecutable = resolveGitExecutable()
|
||||||
|
const shellProbe = spawnSync(["/bin/sh", "-c", "printf '%s' probe"], {
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
})
|
})
|
||||||
|
const gitVersionProbe = spawnSync([gitExecutable, "--version"], {
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
})
|
||||||
|
const directRevParse = spawnSync([gitExecutable, "rev-parse", "--show-toplevel"], {
|
||||||
|
cwd: nestedDirectory,
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
})
|
||||||
|
expect({
|
||||||
|
gitExecutable,
|
||||||
|
exitCode: directRevParse.exitCode,
|
||||||
|
gitVersionOutput: new TextDecoder().decode(gitVersionProbe.stdout).trim().startsWith("git version"),
|
||||||
|
shellOutput: new TextDecoder().decode(shellProbe.stdout),
|
||||||
|
stderr: new TextDecoder().decode(directRevParse.stderr),
|
||||||
|
stdout: new TextDecoder().decode(directRevParse.stdout).trim(),
|
||||||
|
}).toEqual({
|
||||||
|
gitExecutable,
|
||||||
|
exitCode: 0,
|
||||||
|
gitVersionOutput: true,
|
||||||
|
shellOutput: "probe",
|
||||||
|
stderr: "",
|
||||||
|
stdout: canonicalPath(repositoryDir),
|
||||||
|
})
|
||||||
|
expect(detectWorktreePath(nestedDirectory)).toBe(canonicalPath(repositoryDir))
|
||||||
|
|
||||||
writeSkill(
|
writeSkill(
|
||||||
join(repositoryDir, ".claude", "skills", "repo-claude"),
|
join(repositoryDir, ".claude", "skills", "repo-claude"),
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import fs from "node:fs/promises"
|
|||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
|
|
||||||
import type { TeamModeConfig } from "./manager"
|
import type { TeamModeConfig } from "./manager"
|
||||||
|
import { resolveGitExecutable } from "../../../shared"
|
||||||
import { spawn as bunSpawn } from "../../../shared/bun-spawn-shim"
|
import { spawn as bunSpawn } from "../../../shared/bun-spawn-shim"
|
||||||
|
|
||||||
async function runGit(args: string[]): Promise<{ code: number; stderr: string }> {
|
async function runGit(args: string[]): Promise<{ code: number; stderr: string }> {
|
||||||
const process = bunSpawn({ cmd: ["git", ...args], stdout: "pipe", stderr: "pipe" })
|
const process = bunSpawn({ cmd: [resolveGitExecutable(), ...args], stdout: "pipe", stderr: "pipe" })
|
||||||
const [exitCode, stderrText] = await Promise.all([process.exited, new Response(process.stderr).text()])
|
const [exitCode, stderrText] = await Promise.all([process.exited, new Response(process.stderr).text()])
|
||||||
return { code: exitCode, stderr: stderrText }
|
return { code: exitCode, stderr: stderrText }
|
||||||
}
|
}
|
||||||
@@ -14,7 +15,7 @@ export async function removeWorktree(worktreePath: string): Promise<void> {
|
|||||||
await fs.rm(worktreePath, { recursive: true, force: true })
|
await fs.rm(worktreePath, { recursive: true, force: true })
|
||||||
|
|
||||||
const rootLookup = bunSpawn({
|
const rootLookup = bunSpawn({
|
||||||
cmd: ["git", "-C", worktreePath, "rev-parse", "--show-superproject-working-tree"],
|
cmd: [resolveGitExecutable(), "-C", worktreePath, "rev-parse", "--show-superproject-working-tree"],
|
||||||
stdout: "pipe",
|
stdout: "pipe",
|
||||||
stderr: "pipe",
|
stderr: "pipe",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,18 +14,21 @@ import {
|
|||||||
validateWorktreeSpec,
|
validateWorktreeSpec,
|
||||||
} from "./manager"
|
} from "./manager"
|
||||||
import { removeWorktree } from "./cleanup"
|
import { removeWorktree } from "./cleanup"
|
||||||
|
import { spawnSync } from "../../../shared/bun-spawn-shim"
|
||||||
|
import { resolveGitExecutable } from "../../../shared"
|
||||||
|
|
||||||
const temporaryDirectories: string[] = []
|
const temporaryDirectories: string[] = []
|
||||||
|
|
||||||
async function initGitRepo(): Promise<string> {
|
async function initGitRepo(): Promise<string> {
|
||||||
const repositoryRoot = await fs.mkdtemp(path.join(tmpdir(), "team-worktree-"))
|
const repositoryRoot = await fs.mkdtemp(path.join(tmpdir(), "team-worktree-"))
|
||||||
temporaryDirectories.push(repositoryRoot)
|
temporaryDirectories.push(repositoryRoot)
|
||||||
Bun.spawnSync(["git", "init"], { cwd: repositoryRoot, stdout: "pipe", stderr: "pipe" })
|
const git = resolveGitExecutable()
|
||||||
|
spawnSync([git, "init"], { cwd: repositoryRoot, stdout: "pipe", stderr: "pipe" })
|
||||||
await fs.writeFile(path.join(repositoryRoot, "README.md"), "hello\n")
|
await fs.writeFile(path.join(repositoryRoot, "README.md"), "hello\n")
|
||||||
Bun.spawnSync(["git", "add", "README.md"], { cwd: repositoryRoot, stdout: "pipe", stderr: "pipe" })
|
spawnSync([git, "add", "README.md"], { cwd: repositoryRoot, stdout: "pipe", stderr: "pipe" })
|
||||||
Bun.spawnSync(["git", "config", "user.email", "test@example.com"], { cwd: repositoryRoot, stdout: "pipe", stderr: "pipe" })
|
spawnSync([git, "config", "user.email", "test@example.com"], { cwd: repositoryRoot, stdout: "pipe", stderr: "pipe" })
|
||||||
Bun.spawnSync(["git", "config", "user.name", "Test User"], { cwd: repositoryRoot, stdout: "pipe", stderr: "pipe" })
|
spawnSync([git, "config", "user.name", "Test User"], { cwd: repositoryRoot, stdout: "pipe", stderr: "pipe" })
|
||||||
Bun.spawnSync(["git", "commit", "-m", "init"], { cwd: repositoryRoot, stdout: "pipe", stderr: "pipe" })
|
spawnSync([git, "commit", "-m", "init"], { cwd: repositoryRoot, stdout: "pipe", stderr: "pipe" })
|
||||||
return repositoryRoot
|
return repositoryRoot
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,10 +60,11 @@ describe("team-worktree manager", () => {
|
|||||||
// then
|
// then
|
||||||
expect(resultPath).toBe(worktreeDirectory)
|
expect(resultPath).toBe(worktreeDirectory)
|
||||||
await expect(fs.stat(worktreeDirectory)).resolves.toBeDefined()
|
await expect(fs.stat(worktreeDirectory)).resolves.toBeDefined()
|
||||||
const listResult = Bun.spawnSync(["git", "worktree", "list"], { cwd: repositoryRoot, stdout: "pipe", stderr: "pipe" })
|
const git = resolveGitExecutable()
|
||||||
|
const listResult = spawnSync([git, "worktree", "list"], { cwd: repositoryRoot, stdout: "pipe", stderr: "pipe" })
|
||||||
expect(new TextDecoder().decode(listResult.stdout)).toContain(worktreeDirectory)
|
expect(new TextDecoder().decode(listResult.stdout)).toContain(worktreeDirectory)
|
||||||
const headResult = Bun.spawnSync(["git", "-C", worktreeDirectory, "rev-parse", "HEAD"], { stdout: "pipe", stderr: "pipe" })
|
const headResult = spawnSync([git, "-C", worktreeDirectory, "rev-parse", "HEAD"], { stdout: "pipe", stderr: "pipe" })
|
||||||
const repoHeadResult = Bun.spawnSync(["git", "-C", repositoryRoot, "rev-parse", "HEAD"], { stdout: "pipe", stderr: "pipe" })
|
const repoHeadResult = spawnSync([git, "-C", repositoryRoot, "rev-parse", "HEAD"], { stdout: "pipe", stderr: "pipe" })
|
||||||
expect(new TextDecoder().decode(headResult.stdout).trim()).toBe(new TextDecoder().decode(repoHeadResult.stdout).trim())
|
expect(new TextDecoder().decode(headResult.stdout).trim()).toBe(new TextDecoder().decode(repoHeadResult.stdout).trim())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
|
import { resolveGitExecutable } from "../../../shared"
|
||||||
import { spawn as bunSpawn } from "../../../shared/bun-spawn-shim"
|
import { spawn as bunSpawn } from "../../../shared/bun-spawn-shim"
|
||||||
|
|
||||||
export type TeamModeConfig = {
|
export type TeamModeConfig = {
|
||||||
@@ -17,7 +18,7 @@ function countParentSegments(spec: string): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function runGit(args: string[], cwd?: string): Promise<{ code: number; stderr: string }> {
|
async function runGit(args: string[], cwd?: string): Promise<{ code: number; stderr: string }> {
|
||||||
const process = bunSpawn({ cmd: ["git", ...args], cwd, stdout: "pipe", stderr: "pipe" })
|
const process = bunSpawn({ cmd: [resolveGitExecutable(), ...args], cwd, stdout: "pipe", stderr: "pipe" })
|
||||||
const [exitCode, stderrBytes] = await Promise.all([process.exited, new Response(process.stderr).text()])
|
const [exitCode, stderrBytes] = await Promise.all([process.exited, new Response(process.stderr).text()])
|
||||||
return { code: exitCode, stderr: stderrBytes }
|
return { code: exitCode, stderr: stderrBytes }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { existsSync } from "node:fs"
|
||||||
|
|
||||||
|
const GIT_EXECUTABLE_CANDIDATES = [
|
||||||
|
"/usr/bin/git",
|
||||||
|
"/opt/homebrew/bin/git",
|
||||||
|
"/usr/local/bin/git",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export function resolveGitExecutable(): string {
|
||||||
|
for (const candidate of GIT_EXECUTABLE_CANDIDATES) {
|
||||||
|
if (existsSync(candidate)) {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof Bun !== "undefined") {
|
||||||
|
return Bun.which("git") ?? "git"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "git"
|
||||||
|
}
|
||||||
@@ -61,6 +61,7 @@ export * from "./opencode-provider-auth"
|
|||||||
export * from "./opencode-http-api"
|
export * from "./opencode-http-api"
|
||||||
export * from "./port-utils"
|
export * from "./port-utils"
|
||||||
export * from "./git-worktree"
|
export * from "./git-worktree"
|
||||||
|
export * from "./git-executable"
|
||||||
export * from "./safe-create-hook"
|
export * from "./safe-create-hook"
|
||||||
export * from "./truncate-description"
|
export * from "./truncate-description"
|
||||||
export * from "./opencode-storage-paths"
|
export * from "./opencode-storage-paths"
|
||||||
|
|||||||
@@ -1,51 +1,83 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test"
|
/// <reference types="bun-types" />
|
||||||
|
|
||||||
|
import { afterEach, beforeEach, describe, expect, it } from "bun:test"
|
||||||
import { mkdirSync, realpathSync, rmSync, writeFileSync } from "node:fs"
|
import { mkdirSync, realpathSync, rmSync, writeFileSync } from "node:fs"
|
||||||
import { tmpdir } from "node:os"
|
import { tmpdir } from "node:os"
|
||||||
import { join } from "node:path"
|
import { join } from "node:path"
|
||||||
|
|
||||||
|
import { spawnSync } from "./bun-spawn-shim"
|
||||||
|
import { resolveGitExecutable } from "./git-executable"
|
||||||
|
|
||||||
const TEST_DIR = join(tmpdir(), `project-discovery-dirs-${Date.now()}`)
|
const TEST_DIR = join(tmpdir(), `project-discovery-dirs-${Date.now()}`)
|
||||||
let worktreeSpawnCount = 0
|
type ProjectDiscoveryDirsModule = typeof import("./project-discovery-dirs")
|
||||||
|
|
||||||
|
async function importFreshProjectDiscoveryDirs(): Promise<ProjectDiscoveryDirsModule> {
|
||||||
|
return import(`./project-discovery-dirs?test=${Date.now()}-${Math.random()}`)
|
||||||
|
}
|
||||||
|
|
||||||
function canonicalPath(path: string): string {
|
function canonicalPath(path: string): string {
|
||||||
return realpathSync(path)
|
return realpathSync(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function runGit(args: string[], cwd: string): void {
|
||||||
|
const result = spawnSync([resolveGitExecutable(), ...args], {
|
||||||
|
cwd,
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
})
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
throw new Error(new TextDecoder().decode(result.stderr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
describe("project-discovery-dirs", () => {
|
describe("project-discovery-dirs", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mkdirSync(TEST_DIR, { recursive: true })
|
mkdirSync(TEST_DIR, { recursive: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
mock.restore()
|
|
||||||
rmSync(TEST_DIR, { recursive: true, force: true })
|
rmSync(TEST_DIR, { recursive: true, force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
it("#given repeated worktree detection #when detecting twice #then reuses the cached result", async () => {
|
it("#given repeated worktree detection #when detecting twice #then reuses the cached result", async () => {
|
||||||
// given
|
// given
|
||||||
worktreeSpawnCount = 0
|
const repositoryDir = join(TEST_DIR, "repo")
|
||||||
|
const nestedDirectory = join(repositoryDir, "packages", "app")
|
||||||
mock.module("node:child_process", () => ({
|
mkdirSync(nestedDirectory, { recursive: true })
|
||||||
execFileSync: () => {
|
runGit(["init"], repositoryDir)
|
||||||
worktreeSpawnCount += 1
|
|
||||||
return TEST_DIR
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
const { clearWorktreeCache, detectWorktreePath } = await import("./project-discovery-dirs")
|
const { clearWorktreeCache, detectWorktreePath } = await import("./project-discovery-dirs")
|
||||||
|
|
||||||
clearWorktreeCache()
|
clearWorktreeCache()
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const firstPath = detectWorktreePath("/some/dir")
|
const firstPath = detectWorktreePath(nestedDirectory)
|
||||||
const secondPath = detectWorktreePath("/some/dir")
|
rmSync(join(repositoryDir, ".git"), { recursive: true, force: true })
|
||||||
|
const secondPath = detectWorktreePath(nestedDirectory)
|
||||||
clearWorktreeCache()
|
clearWorktreeCache()
|
||||||
const thirdPath = detectWorktreePath("/some/dir")
|
const thirdPath = detectWorktreePath(nestedDirectory)
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(firstPath).toBe(TEST_DIR)
|
expect(firstPath).toBe(canonicalPath(repositoryDir))
|
||||||
expect(secondPath).toBe(TEST_DIR)
|
expect(secondPath).toBe(firstPath)
|
||||||
expect(thirdPath).toBe(TEST_DIR)
|
expect(thirdPath).toBeUndefined()
|
||||||
expect(worktreeSpawnCount).toBe(2)
|
})
|
||||||
|
|
||||||
|
it("#given a fresh module and real git repo #when detecting a worktree #then resolves the repository root", async () => {
|
||||||
|
// given
|
||||||
|
const repositoryDir = join(TEST_DIR, "fresh-repo")
|
||||||
|
const nestedDirectory = join(repositoryDir, "packages", "app")
|
||||||
|
mkdirSync(nestedDirectory, { recursive: true })
|
||||||
|
runGit(["init"], repositoryDir)
|
||||||
|
|
||||||
|
const { clearWorktreeCache, detectWorktreePath } = await importFreshProjectDiscoveryDirs()
|
||||||
|
clearWorktreeCache()
|
||||||
|
|
||||||
|
// when
|
||||||
|
const worktreePath = detectWorktreePath(nestedDirectory)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(worktreePath).toBe(canonicalPath(repositoryDir))
|
||||||
})
|
})
|
||||||
|
|
||||||
it("#given nested .opencode skill directories #when finding project opencode skill dirs #then returns nearest-first with aliases", async () => {
|
it("#given nested .opencode skill directories #when finding project opencode skill dirs #then returns nearest-first with aliases", async () => {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { execFileSync } from "node:child_process"
|
|
||||||
import { existsSync, realpathSync } from "node:fs"
|
import { existsSync, realpathSync } from "node:fs"
|
||||||
import { dirname, join, resolve } from "node:path"
|
import { dirname, join, resolve } from "node:path"
|
||||||
|
|
||||||
|
import { spawnSync } from "./bun-spawn-shim"
|
||||||
|
import { resolveGitExecutable } from "./git-executable"
|
||||||
import { detectPluginConfigFile } from "./jsonc-parser"
|
import { detectPluginConfigFile } from "./jsonc-parser"
|
||||||
import { CONFIG_BASENAME, LEGACY_CONFIG_BASENAME } from "./plugin-identity"
|
import { CONFIG_BASENAME, LEGACY_CONFIG_BASENAME } from "./plugin-identity"
|
||||||
|
|
||||||
@@ -64,20 +65,34 @@ export function detectWorktreePath(directory: string): string | undefined {
|
|||||||
return worktreePathCache.get(resolvedDirectory)
|
return worktreePathCache.get(resolvedDirectory)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let result: ReturnType<typeof spawnSync>
|
||||||
try {
|
try {
|
||||||
const worktreePath = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
result = spawnSync([resolveGitExecutable(), "rev-parse", "--show-toplevel"], {
|
||||||
cwd: resolvedDirectory,
|
cwd: resolvedDirectory,
|
||||||
encoding: "utf-8",
|
stdout: "pipe",
|
||||||
timeout: 5000,
|
stderr: "pipe",
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
})
|
||||||
}).trim()
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
worktreePathCache.set(resolvedDirectory, undefined)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
worktreePathCache.set(resolvedDirectory, worktreePath)
|
if (result.exitCode !== 0 || result.stdout === undefined) {
|
||||||
return worktreePath
|
|
||||||
} catch {
|
|
||||||
worktreePathCache.set(resolvedDirectory, undefined)
|
worktreePathCache.set(resolvedDirectory, undefined)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const worktreePath = result.stdout.toString("utf8").trim()
|
||||||
|
if (worktreePath === "") {
|
||||||
|
worktreePathCache.set(resolvedDirectory, undefined)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
worktreePathCache.set(resolvedDirectory, worktreePath)
|
||||||
|
return worktreePath
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findProjectClaudeSkillDirs(startDirectory: string, stopDirectory?: string): string[] {
|
export function findProjectClaudeSkillDirs(startDirectory: string, stopDirectory?: string): string[] {
|
||||||
|
|||||||
Reference in New Issue
Block a user