import { tmpdir } from "node:os"; import { resolve, win32 } from "node:path"; import { parse } from "@aliou/sh"; import { expandHomePath, maybePathLike } from "../../core/paths/path"; import { walkCommands, wordToString } from "../../core/shell/ast"; import { classifyCommandArgs } from "../../core/shell/command-args"; import { expandGlob, hasGlobChars } from "../glob"; /** * Translate MSYS/Git Bash absolute path forms to native Windows paths. * * Pi's bash tool on Windows is Git Bash, which emits `/c/Users/...` for * drive paths and `/tmp/...` for its temp directory. Feeding those to * path.win32.resolve would glue them onto the cwd's drive * (`C:\cwd\c\Users\...`), so policies would check the wrong file. * No-op on other platforms. */ export function translateMsysPath(input: string): string { if (process.platform !== "win32") return input; const drive = /^\/([a-zA-Z])(?:\/|$)/.exec(input); if (drive?.[1]) { const rest = input.slice(drive[0].length).replaceAll("/", "\\"); return `${drive[1].toUpperCase()}:\\${rest}`; } if (input === "/tmp" || input.startsWith("/tmp/")) { return win32.join(tmpdir(), input.slice("/tmp".length)); } return input; } async function expandCandidate( candidate: string, cwd: string, ): Promise { if (!hasGlobChars(candidate)) return [candidate]; const matches = await expandGlob(candidate, { cwd }); return matches.length > 0 ? matches : [candidate]; } /** * Extract path-like candidates from a bash command string. * Returns absolute paths. Best-effort: uses AST parsing with regex fallback. * Does NOT filter by any policy — returns all path-like arguments. */ export async function extractBashPathCandidates( command: string, cwd: string, ): Promise { const seen = new Set(); const results: string[] = []; const addCandidate = async ( token: string, forcePath = false, ): Promise => { if (!token || token.startsWith("-")) return; if (!forcePath && !maybePathLike(token)) return; const expanded = await expandCandidate(token, cwd); for (const file of expanded) { const abs = resolve(cwd, translateMsysPath(expandHomePath(file))); if (!seen.has(abs)) { seen.add(abs); results.push(abs); } } }; try { const { ast } = parse(command); const pending: Promise[] = []; walkCommands(ast, (cmd) => { const words = (cmd.words ?? []).map(wordToString); const commandName = words[0]; if (commandName) { for (const arg of classifyCommandArgs(commandName, words.slice(1))) { pending.push(addCandidate(arg.token, arg.forcePath)); } } for (const redir of cmd.redirects ?? []) { pending.push(addCandidate(wordToString(redir.target), true)); } return false; }); await Promise.all(pending); return results; } catch { // Fallback: regex tokenization const tokenRegex = /"([^"]+)"|'([^']+)'|`([^`]+)`|([^\s"'`<>|;&]+)/g; for (const match of command.matchAll(tokenRegex)) { const token = match[1] ?? match[2] ?? match[3] ?? match[4] ?? ""; if (token && !token.startsWith("-") && maybePathLike(token)) { const expanded = await expandCandidate(token, cwd); for (const file of expanded) { const abs = resolve(cwd, translateMsysPath(expandHomePath(file))); if (!seen.has(abs)) { seen.add(abs); results.push(abs); } } } } return results; } }