import { rmSync } from "node:fs"; import { chmod, mkdtemp, readFile, realpath, writeFile, } from "node:fs/promises"; import { tmpdir } from "node:os"; import { delimiter, join } from "node:path"; import { findExecutable } from "./src/pi-ext-executable.ts"; const POLICY_MARKER = "# pi-git-safe-policy-v1"; const COMMAND_PREFIX_END = "# pi-command-prefix-end"; export interface GitPolicyRuntime { wrapperDir: string; realGit: string; } function pathKey(env: NodeJS.ProcessEnv): string { return Object.keys(env).find((key) => key.toLowerCase() === "path") ?? "PATH"; } export async function findGitExecutable( env: NodeJS.ProcessEnv = process.env, platform = process.platform, ): Promise { const path = env[pathKey(env)] ?? ""; const names = platform === "win32" ? ["git.exe"] : ["git"]; for (const dir of path.split(delimiter).filter(Boolean)) { const candidate = findExecutable(names, { path: dir.replace(/^"|"$/gu, ""), platform, }); if (!candidate) continue; try { return await realpath(candidate); } catch { // Try the next PATH entry if canonicalization fails. } } return undefined; } const RUNTIME_CACHE = Symbol.for("@bugabinga/pi-ext-git-safe/runtime-cache-v1"); type RuntimeCache = { promises: Map>; wrapperDirs: Set; cleanupRegistered: boolean; }; type RuntimeGlobal = typeof globalThis & { [RUNTIME_CACHE]?: RuntimeCache }; const runtimeGlobal = globalThis as RuntimeGlobal; const existingRuntimeCache = runtimeGlobal[RUNTIME_CACHE]; const runtimeCache: RuntimeCache = existingRuntimeCache ?? { promises: new Map(), wrapperDirs: new Set(), cleanupRegistered: false, }; if (!existingRuntimeCache) runtimeGlobal[RUNTIME_CACHE] = runtimeCache; if (!runtimeCache.cleanupRegistered) { runtimeCache.cleanupRegistered = true; process.once("exit", () => { for (const wrapperDir of runtimeCache.wrapperDirs) { try { rmSync(wrapperDir, { recursive: true, force: true }); } catch {} } }); } async function generateGitPolicyRuntime( env: NodeJS.ProcessEnv, platform: NodeJS.Platform, ): Promise { const realGit = await findGitExecutable(env, platform); if (!realGit) throw new Error("Git executable not found on PATH"); const wrapperDir = await mkdtemp(join(tmpdir(), "pi-git-safe-policy-")); const wrapperScript = join(wrapperDir, "git-wrapper.cjs"); const launcher = join(wrapperDir, "git"); const wrapperScriptArgument = platform === "win32" ? wrapperScript : bashPath(wrapperScript); try { const template = await readFile( new URL("./git-wrapper.cjs", import.meta.url), "utf8", ); await Promise.all([ writeFile( wrapperScript, template.replace('"__PI_GIT_SAFE_REAL_GIT__"', JSON.stringify(realGit)), { encoding: "utf8", mode: 0o600 }, ), writeFile( launcher, `#!/bin/sh if [ ! -f ${shellQuote(bashPath(wrapperScript))} ]; then printf '%s\\n' 'git: command unavailable; ask the user to restart Pi' >&2 exit 127 fi exec ${shellQuote(bashPath(process.execPath))} ${shellQuote(wrapperScriptArgument)} "$@" `, { encoding: "utf8", mode: 0o500 }, ), writeFile( join(wrapperDir, "git.cmd"), `@echo off\r\n"${process.execPath}" "%~dp0git-wrapper.cjs" %*\r\n`, { encoding: "utf8", mode: 0o500 }, ), ]); await chmod(launcher, 0o500); runtimeCache.wrapperDirs.add(wrapperDir); return { wrapperDir, realGit }; } catch (error) { rmSync(wrapperDir, { recursive: true, force: true }); throw error; } } export function createGitPolicyRuntime( env: NodeJS.ProcessEnv = process.env, platform: NodeJS.Platform = process.platform, ): Promise { const key = `${platform}\0${process.execPath}\0${env[pathKey(env)] ?? ""}`; const existing = runtimeCache.promises.get(key); if (existing) return existing; const runtime = generateGitPolicyRuntime(env, platform).catch((error) => { runtimeCache.promises.delete(key); throw error; }); runtimeCache.promises.set(key, runtime); return runtime; } function shellQuote(value: string): string { return `'${value.replaceAll("'", `'\\''`)}'`; } function bashPath(value: string): string { const normalized = value.replaceAll("\\", "/"); const drive = normalized.match(/^([A-Za-z]):\/(.*)$/u); return drive ? `/${drive[1].toLowerCase()}/${drive[2]}` : normalized; } export function applyGitPolicy( command: string, runtime: GitPolicyRuntime, ): string { const wrapper = shellQuote(bashPath(join(runtime.wrapperDir, "git"))); return `${POLICY_MARKER} export PATH=${shellQuote(bashPath(runtime.wrapperDir))}:"$PATH" git() { ${wrapper} "$@" } git.exe() { ${wrapper} "$@" } command() { local -a __pi_git_safe_args=("$@") local __pi_git_safe_index=0 while [[ "\${__pi_git_safe_args[$__pi_git_safe_index]}" == "-p" || "\${__pi_git_safe_args[$__pi_git_safe_index]}" == "--" ]]; do ((__pi_git_safe_index++)) done if [[ "\${__pi_git_safe_args[$__pi_git_safe_index]}" == "git" || "\${__pi_git_safe_args[$__pi_git_safe_index]}" == "git.exe" ]]; then ${wrapper} "\${__pi_git_safe_args[@]:$((__pi_git_safe_index + 1))}" else builtin command "$@" fi } env() { local -a __pi_git_safe_args=("$@") local __pi_git_safe_index=0 while (( __pi_git_safe_index < \${#__pi_git_safe_args[@]} )); do case "\${__pi_git_safe_args[$__pi_git_safe_index]}" in --) ((__pi_git_safe_index++)) break ;; --u*=*|--c*=*|--s*=*|--a*=*) ((__pi_git_safe_index++)) ;; -u|--u*|-C|--c*|-S|--s*|--a*) ((__pi_git_safe_index += 2)) ;; -*|*=*) ((__pi_git_safe_index++)) ;; *) break ;; esac done if [[ "\${__pi_git_safe_args[$__pi_git_safe_index]}" == "git" || "\${__pi_git_safe_args[$__pi_git_safe_index]}" == "git.exe" ]]; then __pi_git_safe_args[$__pi_git_safe_index]=${wrapper} fi builtin command env "\${__pi_git_safe_args[@]}" } xargs() { local -a __pi_git_safe_args=("$@") local __pi_git_safe_index=0 while (( __pi_git_safe_index < \${#__pi_git_safe_args[@]} )); do case "\${__pi_git_safe_args[$__pi_git_safe_index]}" in --) ((__pi_git_safe_index++)) break ;; --arg*=*|--e*=*|--r*=*|--m*=*|--d*=*) ((__pi_git_safe_index++)) ;; -a|-E|-I|-L|-n|-P|-s|-d|--arg*|--e*|--r*|--m*|--d*) ((__pi_git_safe_index += 2)) ;; -*) ((__pi_git_safe_index++)) ;; *) break ;; esac done if [[ "\${__pi_git_safe_args[$__pi_git_safe_index]}" == "git" || "\${__pi_git_safe_args[$__pi_git_safe_index]}" == "git.exe" ]]; then __pi_git_safe_args[$__pi_git_safe_index]=${wrapper} fi builtin command xargs "\${__pi_git_safe_args[@]}" } export -f git git.exe command env xargs ${COMMAND_PREFIX_END} ${command}`; }