import { existsSync } from "node:fs"; import { realpath } from "node:fs/promises"; import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path"; import { minimatch } from "minimatch"; import { expandHome, type ZoneName } from "./config.ts"; export interface PathContext { workspaceRoots: string[]; sensitivePatterns: string[]; } export interface ClassifiedPath { input: string; path: string; zone: ZoneName; } export function toAbsolute(path: string, cwd: string): string { const expanded = expandHome(path); return normalize(isAbsolute(expanded) ? expanded : resolve(cwd, expanded)); } export async function realpathSafe(path: string): Promise { const absolute = normalize(path); try { return await realpath(absolute); } catch { let current = absolute; const tail: string[] = []; for (;;) { try { const resolved = await realpath(current); return join(resolved, ...tail.reverse()); } catch { const parent = dirname(current); if (parent === current) return absolute; tail.push(basename(current)); current = parent; } } } } export function isInside(parent: string, child: string): boolean { const rel = relative(parent, child); return rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel)); } function normalizeForGlob(path: string): string { return normalize(path).split(sep).join("/"); } function isEnvExample(path: string): boolean { const name = basename(path).toLowerCase(); return name === ".env.example" || name === ".env.sample" || name === ".env.template"; } function matchesSensitive(path: string, pattern: string): boolean { const expanded = expandHome(pattern); if (!/[*?[\]{}]/.test(expanded)) { const absolute = normalize(isAbsolute(expanded) ? expanded : resolve(expanded)); return isInside(absolute, path); } const normalized = normalizeForGlob(path); const glob = normalizeForGlob(expanded); return minimatch(normalized, glob, { dot: true, nocase: process.platform === "win32", matchBase: true }); } export function isSensitive(path: string, patterns: readonly string[]): boolean { if (isEnvExample(path)) return false; return patterns.some((pattern) => matchesSensitive(path, pattern)); } export async function classifyPath( input: string, cwd: string, context: PathContext, ): Promise { const path = await realpathSafe(toAbsolute(input, cwd)); if (isSensitive(path, context.sensitivePatterns)) return { input, path, zone: "sensitive" }; if (context.workspaceRoots.some((root) => isInside(root, path))) { return { input, path, zone: "workspace" }; } return { input, path, zone: "external" }; } export function repositoryRoot(path: string): string | undefined { let current = existsSync(path) ? path : dirname(path); for (;;) { if (existsSync(join(current, ".git"))) return current; const parent = dirname(current); if (parent === current) return undefined; current = parent; } }