repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/uv/index.ts
Rawimport { delimiter, join } from "node:path";
import {
type BashSpawnContext,
createBashTool,
type ExtensionAPI,
} from "@earendil-works/pi-coding-agent";
import { closeDebug, dbg, span } from "./src/debug.ts";
import { findExecutable } from "./src/pi-ext-executable.ts";
const INTERCEPTED_COMMANDS_PATH = join(
import.meta.dirname,
"intercepted-commands",
);
const PIP_COMMAND = /(?:^|\n|[;|&]{1,2})\s*(?:\S+\/)?pip\s*(?:$|\s)/m;
const PIP3_COMMAND = /(?:^|\n|[;|&]{1,2})\s*(?:\S+\/)?pip3\s*(?:$|\s)/m;
const POETRY_COMMAND = /(?:^|\n|[;|&]{1,2})\s*(?:\S+\/)?poetry\s*(?:$|\s)/m;
const PYTHON_COMMAND = String.raw`(?:^|\n|[;|&]{1,2})\s*(?:\S+\/)?python(?:3(?:\.\d+)?)?\b[^\n;|&]*`;
const PYTHON_PIP_COMMAND = new RegExp(
`${PYTHON_COMMAND}(?:\\s-m\\s*pip\\b|\\s-mpip\\b)`,
"m",
);
const PYTHON_VENV_COMMAND = new RegExp(
`${PYTHON_COMMAND}(?:\\s-m\\s*venv\\b|\\s-mvenv\\b)`,
"m",
);
const PYTHON_COMPILE_COMMAND = new RegExp(
`${PYTHON_COMMAND}(?:\\s-m\\s*py_compile\\b|\\s-mpy_compile\\b)`,
"m",
);
async function uvInstalled(
env = process.env,
platform = process.platform,
): Promise<boolean> {
return Boolean(
findExecutable(
platform === "win32" ? ["uv.exe", "uv.cmd", "uv.bat"] : ["uv"],
{ path: env[pathEnvKey(env, platform)] ?? "", platform },
),
);
}
function pathEnvKey(
env: NodeJS.ProcessEnv,
platform = process.platform,
): string {
if (platform !== "win32") return "PATH";
return Object.keys(env).find((key) => key.toLowerCase() === "path") ?? "PATH";
}
function prependInterceptedCommands(
ctx: BashSpawnContext,
platform = process.platform,
): BashSpawnContext {
if (platform === "win32") return ctx;
const key = pathEnvKey(ctx.env);
const path = [INTERCEPTED_COMMANDS_PATH, ctx.env[key]]
.filter(Boolean)
.join(delimiter);
return { ...ctx, env: { ...ctx.env, [key]: path } };
}
function blocked(message: string, ...body: string[]): string {
return [
`Error: ${message}. Use uv instead:`,
"",
...body.map((line) => ` ${line}`),
"",
].join("\n");
}
const PIP_BLOCKED = blocked(
"pip is disabled",
"To install a package for a script: uv run --with PACKAGE python script.py",
"To add a dependency to the project: uv add PACKAGE",
);
const PIP3_BLOCKED = blocked(
"pip3 is disabled",
"To install a package for a script: uv run --with PACKAGE python script.py",
"To add a dependency to the project: uv add PACKAGE",
);
const POETRY_BLOCKED = blocked(
"poetry is disabled",
"To initialize a project: uv init",
"To add a dependency: uv add PACKAGE",
"To sync dependencies: uv sync",
"To run commands: uv run COMMAND",
);
const PYTHON_PIP_BLOCKED = blocked(
"'python -m pip' is disabled",
"To install a package for a script: uv run --with PACKAGE python script.py",
"To add a dependency to the project: uv add PACKAGE",
);
const PYTHON_VENV_BLOCKED = blocked(
"'python -m venv' is disabled",
"To create a virtual environment: uv venv",
);
const PYTHON_COMPILE_BLOCKED = [
"Error: 'python -m py_compile' is disabled because it writes .pyc files to __pycache__.",
"",
" To verify syntax without bytecode output: uv run python -m ast path/to/file.py >/dev/null",
"",
].join("\n");
function getBlockedCommandMessage(command: string): string | null {
if (PIP_COMMAND.test(command)) return PIP_BLOCKED;
if (PIP3_COMMAND.test(command)) return PIP3_BLOCKED;
if (POETRY_COMMAND.test(command)) return POETRY_BLOCKED;
if (PYTHON_PIP_COMMAND.test(command)) return PYTHON_PIP_BLOCKED;
if (PYTHON_VENV_COMMAND.test(command)) return PYTHON_VENV_BLOCKED;
if (PYTHON_COMPILE_COMMAND.test(command)) return PYTHON_COMPILE_BLOCKED;
return null;
}
export const __test = {
getBlockedCommandMessage,
prependInterceptedCommands,
uvInstalled,
};
export default function uvExtension(pi: ExtensionAPI) {
let generation = 0;
pi.on("session_start", (_event, ctx) => {
dbg?.("session.start");
const ticket = ++generation;
const finish = span?.("availability.check");
void uvInstalled().then(
(installed) => {
finish?.("finish", { installed });
if (ticket === generation && !installed && ctx.hasUI)
ctx.ui.notify("uv extension: uv not found on PATH", "warning");
},
(error) => {
finish?.("error");
throw error;
},
);
});
pi.on("session_shutdown", () => {
dbg?.("session.shutdown");
closeDebug();
generation++;
});
const bash = createBashTool(process.cwd(), {
spawnHook: (ctx) => {
const message = getBlockedCommandMessage(ctx.command);
if (message) throw new Error(message);
return prependInterceptedCommands(ctx);
},
});
const promptMetadata = bash as typeof bash & {
promptGuidelines?: readonly string[];
};
Object.assign(promptMetadata, {
description: `${bash.description} This replacement routes PATH-based Python through uv and rejects pip, Poetry, python -m pip, python -m venv, and python -m py_compile.`,
promptGuidelines: [
...(promptMetadata.promptGuidelines ?? []).map((guideline) =>
guideline ===
"You can inspect PI_* environment variables for current model and session details."
? "Use bash to inspect PI_* environment variables for current model and session details."
: guideline,
),
"Use bash with uv instead of pip, pip3, Poetry, python -m pip, or python -m venv for Python package and environment operations; use uv run python -m ast PATH >/dev/null instead of python -m py_compile for syntax checks.",
],
});
pi.registerTool(bash);
}