@earendil-works/pi-coding-agent
Version:
Coding agent CLI with read, bash, edit, write tools and session management
156 lines • 6.71 kB
JavaScript
import { realpathSync, statSync } from "node:fs";
import { homedir } from "node:os";
import { isAbsolute, join, resolve as nodeResolvePath, posix, relative, sep, win32 } from "node:path";
import { fileURLToPath } from "node:url";
import { spawnProcessSync } from "./child-process.js";
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
/**
* Resolve a path to its canonical (real) form, following symlinks.
* Falls back to the raw path if resolution fails (e.g. the target does
* not exist yet), so that callers never crash on missing filesystem
* entries.
*/
export function canonicalizePath(path) {
try {
return realpathSync(path);
}
catch {
return path;
}
}
/**
* Key for comparing filesystem paths: the resolved absolute path, case-folded
* on Windows because Windows paths are case-insensitive (`c:\Users` and
* `C:\Users` are the same directory). NTFS can enable per-directory case
* sensitivity, but it is rarely used outside WSL interop, so it is ignored
* here (https://learn.microsoft.com/en-us/windows/wsl/case-sensitivity).
* Does not touch the filesystem; call `canonicalizePath` first when symlinks
* must compare equal. Use only for comparison, never for display or
* filesystem access.
*/
export function getPathComparisonKey(path) {
const pathApi = getPlatformPathApi();
const resolvedPath = pathApi.resolve(path);
return pathApi === win32 ? resolvedPath.toLowerCase() : resolvedPath;
}
/** `node:path` rules for the current platform, read at call time so tests can simulate Windows. */
function getPlatformPathApi() {
return process.platform === "win32" ? win32 : posix;
}
/** True when both paths name the same location (see `getPathComparisonKey`). */
export function pathsEqual(left, right) {
return getPathComparisonKey(left) === getPathComparisonKey(right);
}
/** True when `child` is `parent` or inside it (see `getPathComparisonKey`). */
export function isPathWithin(parent, child) {
const parentKey = getPathComparisonKey(parent);
const childKey = getPathComparisonKey(child);
if (childKey === parentKey)
return true;
// Filesystem roots (`/`, `C:\`) already end with a separator.
const pathSep = getPlatformPathApi().sep;
return childKey.startsWith(parentKey.endsWith(pathSep) ? parentKey : `${parentKey}${pathSep}`);
}
/** Replace a leading home directory with `~` for display. Relative paths are returned unchanged. */
export function shortenHomePath(path, homeDir = homedir()) {
const pathApi = getPlatformPathApi();
if (!homeDir || !pathApi.isAbsolute(path) || !isPathWithin(homeDir, path))
return path;
const relativePath = pathApi.relative(homeDir, path);
return relativePath ? `~${pathApi.sep}${relativePath}` : "~";
}
export function getFileRevision(path) {
try {
const stats = statSync(path, { bigint: true });
return `${stats.dev}:${stats.ino}:${stats.size}:${stats.mtimeNs}:${stats.ctimeNs}`;
}
catch {
return undefined;
}
}
/**
* Returns true if the value is NOT a package source (npm:, git:, etc.),
* a built-in extension (builtin:), or a remote URL protocol. Bare names,
* relative paths, and file: URLs are considered local.
*/
export function isLocalPath(value) {
const trimmed = value.trim();
// Known non-local prefixes. file: URLs are local paths and are intentionally resolved by resolvePath().
if (trimmed.startsWith("npm:") ||
trimmed.startsWith("git:") ||
trimmed.startsWith("github:") ||
trimmed.startsWith("http:") ||
trimmed.startsWith("https:") ||
trimmed.startsWith("ssh:") ||
trimmed.startsWith("builtin:")) {
return false;
}
return true;
}
/** Convert Git Bash, MSYS, Cygwin, and WSL drive paths to a form native Windows APIs accept. */
export function normalizeWindowsShellPath(filePath) {
if (!filePath.startsWith("/") || filePath.startsWith("//") || filePath.includes("\\"))
return filePath;
const match = filePath.match(/^\/(?:mnt\/|cygdrive\/)?([a-z])(?:\/(.*))?$/i);
if (!match)
return filePath;
const suffix = match[2]?.replaceAll("/", "\\");
return `${match[1].toUpperCase()}:\\${suffix ?? ""}`;
}
export function normalizePath(input, options = {}) {
let normalized = options.trim ? input.trim() : input;
if (options.normalizeUnicodeSpaces) {
normalized = normalized.replace(UNICODE_SPACES, " ");
}
if (options.stripAtPrefix && normalized.startsWith("@")) {
normalized = normalized.slice(1);
}
if (process.platform === "win32") {
normalized = normalizeWindowsShellPath(normalized);
}
if (options.expandTilde ?? true) {
const home = options.homeDir ?? homedir();
if (normalized === "~")
return home;
if (normalized.startsWith("~/") || (process.platform === "win32" && normalized.startsWith("~\\"))) {
return join(home, normalized.slice(2));
}
}
if (/^file:\/\//.test(normalized)) {
return fileURLToPath(normalized);
}
return normalized;
}
export function resolvePath(input, baseDir = process.cwd(), options = {}) {
const normalized = normalizePath(input, options);
const normalizedBaseDir = normalizePath(baseDir);
return isAbsolute(normalized) ? nodeResolvePath(normalized) : nodeResolvePath(normalizedBaseDir, normalized);
}
export function getCwdRelativePath(filePath, cwd) {
const resolvedCwd = resolvePath(cwd);
const resolvedPath = resolvePath(filePath, resolvedCwd);
const relativePath = relative(resolvedCwd, resolvedPath);
const isInsideCwd = relativePath === "" ||
(relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath));
return isInsideCwd ? relativePath || "." : undefined;
}
export function formatPathRelativeToCwdOrAbsolute(filePath, cwd) {
const absolutePath = resolvePath(filePath, cwd);
return (getCwdRelativePath(absolutePath, cwd) ?? absolutePath).split(sep).join("/");
}
export function markPathIgnoredByCloudSync(path) {
const attrs = process.platform === "darwin"
? ["com.dropbox.ignored", "com.apple.fileprovider.ignore#P"]
: process.platform === "linux"
? ["user.com.dropbox.ignored"]
: [];
for (const attr of attrs) {
if (process.platform === "darwin") {
spawnProcessSync("xattr", ["-w", attr, "1", path], { encoding: "utf-8", stdio: "ignore" });
}
else {
spawnProcessSync("setfattr", ["-n", attr, "-v", "1", path], { encoding: "utf-8", stdio: "ignore" });
}
}
}
//# sourceMappingURL=paths.js.map