Files
orca/src/main/local-worktree-filesystem.ts
T
mirror 34fa2cd5ca
Skill update round trip / roundtrip (false, macos-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, macos-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, symlink, latest) (push) Canceled after 0s
Skill update round trip / roundtrip (false, windows-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, windows-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, macos-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, macos-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, ubuntu-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, ubuntu-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, windows-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, windows-latest, symlink, 1.5.17) (push) Canceled after 0s
source snapshot 2026-09-02
2026-09-02 02:03:16 +08:00

117 lines
3.8 KiB
TypeScript

import { runProcess } from '../shared/child-process/run-process'
import { lstat, readFile } from 'node:fs/promises'
import { buildWslExecArgs, quotePosixShell } from '../shared/wsl-login-shell-command'
import { removeHostTree } from './host-tree-removal'
import { toLinuxPath } from './wsl'
import type { ReadPath, StatPath } from './worktree-orphan-gitdir-proof'
export { toHostFilesystemPath, toHostRemovalPath } from './host-tree-removal'
export type LocalWorktreeFilesystemOptions = {
wslDistro?: string
}
type LocalWorktreePathAccess = {
statPath: StatPath
readPath: ReadPath
}
const WSL_FILE_OPERATION_TIMEOUT_MS = 30_000
/** The stat probe's explicit "missing path" branch. */
const WSL_MISSING_PATH_EXIT_CODE = 2
function shouldUseWslFilesystem(options: LocalWorktreeFilesystemOptions): boolean {
return process.platform === 'win32' && !!options.wslDistro?.trim()
}
/**
* Run a filesystem command inside the distro.
*
* Why no login shell: these are coreutils at standard paths plus shell builtins,
* and need nothing from the user's PATH. A login shell would only add its rc/motd
* output to the stdout these callers parse -- the banner problem -- so the fix is
* to not start one rather than to fence what it prints.
*/
async function runWslCommand(distro: string, command: string): Promise<string> {
const result = await runProcess({
program: 'wsl.exe',
args: buildWslExecArgs(distro, ['sh', '-c', command]),
timeoutMs: WSL_FILE_OPERATION_TIMEOUT_MS
})
if (result.timedOut) {
throw new Error(`WSL filesystem command timed out after ${WSL_FILE_OPERATION_TIMEOUT_MS}ms`)
}
if (result.code !== 0) {
throw Object.assign(new Error(result.stderr.trim() || `wsl.exe exited ${result.code}`), {
exitCode: result.code
})
}
return result.stdout
}
function isWslMissingPathError(error: unknown): boolean {
return (
typeof error === 'object' &&
error !== null &&
(error as { exitCode?: unknown }).exitCode === WSL_MISSING_PATH_EXIT_CODE
)
}
export function toLocalWorktreeRuntimePath(
targetPath: string,
options: LocalWorktreeFilesystemOptions = {}
): string {
return shouldUseWslFilesystem(options) ? toLinuxPath(targetPath) : targetPath
}
export function getLocalWorktreePathAccess(
options: LocalWorktreeFilesystemOptions = {}
): LocalWorktreePathAccess {
const distro = options.wslDistro?.trim()
if (!shouldUseWslFilesystem(options) || !distro) {
return {
statPath: lstat,
readPath: (path) => readFile(path, 'utf8')
}
}
return {
statPath: async (path) => {
const target = quotePosixShell(toLinuxPath(path))
const stdout = await runWslCommand(
distro,
[
`target=${target}`,
'if [ -L "$target" ]; then printf symlink; elif [ -f "$target" ]; then printf file; elif [ -d "$target" ]; then printf directory; else exit 2; fi'
].join('\n')
).catch((error) => {
if (isWslMissingPathError(error)) {
throw Object.assign(new Error(`missing ${path}`), { code: 'ENOENT' })
}
throw error
})
return { type: stdout.trim() }
},
readPath: async (path) => {
const target = quotePosixShell(toLinuxPath(path))
const stdout = await runWslCommand(distro, `cat -- ${target}`)
return stdout
}
}
}
export async function removeLocalWorktreePath(
targetPath: string,
options: LocalWorktreeFilesystemOptions = {}
): Promise<void> {
const distro = options.wslDistro?.trim()
if (!shouldUseWslFilesystem(options) || !distro) {
await removeHostTree(targetPath)
return
}
// Why: WSL-owned worktree directories may be POSIX paths that Node on
// Windows cannot delete safely. Run the deletion inside the selected distro.
await runWslCommand(distro, `rm -rf -- ${quotePosixShell(toLinuxPath(targetPath))}`)
}