Repository navigation
Expand file tree
/
Copy pathuv.ts
More file actions
123 lines (110 loc) · 4.29 KB
/
Copy pathuv.ts
File metadata and controls
123 lines (110 loc) · 4.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
/**
* UV Extension - Redirects Python tooling to uv equivalents
*
* This extension wraps the bash tool to prepend intercepted-commands to PATH,
* which contains shim scripts that intercept common Python tooling commands
* and redirect agents to use uv instead.
*
* Intercepted commands:
* - pip/pip3: Blocked with suggestions to use `uv add` or `uv run --with`
* - poetry: Blocked with uv equivalents (uv init, uv add, uv sync, uv run)
* - python/python3: Redirected through `uv run` to a real interpreter path,
* with special handling to block `python -m pip`, `python -m venv`, and
* `python -m py_compile`
*
* The shim scripts are located in the intercepted-commands directory and
* provide helpful error messages with the equivalent uv commands.
*
* Note: PATH shims are bypassable via explicit interpreter paths
* (for example `.venv/bin/python`). To close that gap, this extension also
* blocks disallowed invocations at bash spawn time.
*/
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { createBashTool } from "@earendil-works/pi-coding-agent";
import { dirname, join } from "path";
import { fileURLToPath } from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const interceptedCommandsPath = join(__dirname, "..", "intercepted-commands");
function getBlockedCommandMessage(command: string): string | null {
// Match commands at the start of a shell segment (start/newline/; /&& /|| /|)
const pipCommandPattern = /(?:^|\n|[;|&]{1,2})\s*(?:\S+\/)?pip\s*(?:$|\s)/m;
const pip3CommandPattern = /(?:^|\n|[;|&]{1,2})\s*(?:\S+\/)?pip3\s*(?:$|\s)/m;
const poetryCommandPattern = /(?:^|\n|[;|&]{1,2})\s*(?:\S+\/)?poetry\s*(?:$|\s)/m;
// Match python invocations including explicit paths like .venv/bin/python
// and .venv/bin/python3.12.
const pythonPipPattern =
/(?:^|\n|[;|&]{1,2})\s*(?:\S+\/)?python(?:3(?:\.\d+)?)?\b[^\n;|&]*(?:\s-m\s*pip\b|\s-mpip\b)/m;
const pythonVenvPattern =
/(?:^|\n|[;|&]{1,2})\s*(?:\S+\/)?python(?:3(?:\.\d+)?)?\b[^\n;|&]*(?:\s-m\s*venv\b|\s-mvenv\b)/m;
const pythonPyCompilePattern =
/(?:^|\n|[;|&]{1,2})\s*(?:\S+\/)?python(?:3(?:\.\d+)?)?\b[^\n;|&]*(?:\s-m\s*py_compile\b|\s-mpy_compile\b)/m;
if (pipCommandPattern.test(command)) {
return [
"Error: pip is disabled. Use uv instead:",
"",
" To install a package for a script: uv run --with PACKAGE python script.py",
" To add a dependency to the project: uv add PACKAGE",
"",
].join("\n");
}
if (pip3CommandPattern.test(command)) {
return [
"Error: pip3 is disabled. Use uv instead:",
"",
" To install a package for a script: uv run --with PACKAGE python script.py",
" To add a dependency to the project: uv add PACKAGE",
"",
].join("\n");
}
if (poetryCommandPattern.test(command)) {
return [
"Error: poetry is disabled. Use uv instead:",
"",
" To initialize a project: uv init",
" To add a dependency: uv add PACKAGE",
" To sync dependencies: uv sync",
" To run commands: uv run COMMAND",
"",
].join("\n");
}
if (pythonPipPattern.test(command)) {
return [
"Error: 'python -m pip' is disabled. Use uv instead:",
"",
" To install a package for a script: uv run --with PACKAGE python script.py",
" To add a dependency to the project: uv add PACKAGE",
"",
].join("\n");
}
if (pythonVenvPattern.test(command)) {
return [
"Error: 'python -m venv' is disabled. Use uv instead:",
"",
" To create a virtual environment: uv venv",
"",
].join("\n");
}
if (pythonPyCompilePattern.test(command)) {
return [
"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");
}
return null;
}
export default function (pi: ExtensionAPI) {
const cwd = process.cwd();
const bashTool = createBashTool(cwd, {
commandPrefix: `export PATH="${interceptedCommandsPath}:$PATH"`,
spawnHook: (ctx) => {
const blockedMessage = getBlockedCommandMessage(ctx.command);
if (blockedMessage) {
throw new Error(blockedMessage);
}
return ctx;
},
});
pi.registerTool(bashTool);
}