repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/git-safe/git-policy.ts
Rawimport { 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<string | undefined> {
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<string, Promise<GitPolicyRuntime>>;
wrapperDirs: Set<string>;
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<GitPolicyRuntime> {
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<GitPolicyRuntime> {
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}`;
}