pi-python-helper 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/package.json +2 -2
- package/src/core/safety.ts +32 -104
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ does not guarantee a stable public tool schema.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.1] - 2026-09-21
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- `src/core/safety.ts` now delegates to `pi-helper-core`'s classifier and supplies only the Python package-manager, environment, and migration rules; the segment-splitting, safe-override precedence, and compound-merge logic is no longer duplicated. Requires `pi-helper-core` 0.1.2, which stops treating `--frozen`/`--locked`/`--list` as read-only flags so `uv sync --frozen` is classified as mutating again.
|
|
15
|
+
|
|
10
16
|
## [0.4.0] - 2026-09-21
|
|
11
17
|
|
|
12
18
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-python-helper",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Python (uv) development tools for the pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -71,6 +71,6 @@
|
|
|
71
71
|
"typescript": "^7.0.2"
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
|
-
"pi-helper-core": "^0.1.
|
|
74
|
+
"pi-helper-core": "^0.1.2"
|
|
75
75
|
}
|
|
76
76
|
}
|
package/src/core/safety.ts
CHANGED
|
@@ -1,77 +1,56 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* Python's risk rules for the shared classifier.
|
|
3
|
+
*
|
|
2
4
|
* Python has no equivalent of a `cmd_vel` topic that deterministically signals
|
|
3
|
-
* actuation, so risk
|
|
4
|
-
*
|
|
5
|
-
*
|
|
5
|
+
* actuation, so risk is read from the command text. Segment splitting, safe
|
|
6
|
+
* override precedence, and the compound-command merge live in
|
|
7
|
+
* `pi-helper-core`; this module supplies only the package-manager, environment,
|
|
8
|
+
* and migration rules that are specific to Python. The universal rules (git
|
|
9
|
+
* history, file deletion, destructive SQL, containers, pipe-to-shell) are
|
|
10
|
+
* applied by the core and must not be repeated here.
|
|
6
11
|
*/
|
|
7
|
-
|
|
12
|
+
import {
|
|
13
|
+
classifyCommand as coreClassifyCommand,
|
|
14
|
+
isMutatingCommand as coreIsMutatingCommand,
|
|
15
|
+
riskOf as coreRiskOf,
|
|
16
|
+
type CommandRisk,
|
|
17
|
+
type RiskRule,
|
|
18
|
+
type SafetyRules,
|
|
19
|
+
} from 'pi-helper-core';
|
|
8
20
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
interface RiskPattern {
|
|
12
|
-
risk: Exclude<CommandRisk, 'read'>;
|
|
13
|
-
pattern: RegExp;
|
|
14
|
-
reason: string;
|
|
15
|
-
}
|
|
21
|
+
export { splitCommandSegments } from 'pi-helper-core';
|
|
22
|
+
export type { CommandClassification, CommandRisk } from 'pi-helper-core';
|
|
16
23
|
|
|
17
24
|
/**
|
|
18
|
-
* Safe overrides are matched before risk patterns so a read-only
|
|
19
|
-
* inherit the risk of the command it qualifies
|
|
25
|
+
* Safe overrides are matched before risk patterns so a read-only form does not
|
|
26
|
+
* inherit the risk of the command it qualifies. Only the Python-specific
|
|
27
|
+
* read-only forms live here; `--check`, `--dry-run`, and `--collect-only` are
|
|
28
|
+
* already universal overrides in the core.
|
|
20
29
|
*/
|
|
21
|
-
const
|
|
22
|
-
/\buv\s+lock\b[^&|;]*--check\b/,
|
|
23
|
-
/\buv\s+sync\b[^&|;]*--dry-run\b/,
|
|
30
|
+
const PYTHON_SAFE_OVERRIDES: RegExp[] = [
|
|
24
31
|
/\buv\s+(?:tree|export|version|help)\b/,
|
|
25
32
|
/\buv\s+pip\s+(?:list|freeze|check)\b/,
|
|
26
|
-
/\bpytest\b[^&|;]*--collect-only\b/,
|
|
27
33
|
/\bruff\s+(?:check|format)\b[^&|;]*(?:--diff|--check|--no-cache)\b/,
|
|
28
34
|
/\bmypy\b[^&|;]*--no-incremental\b/,
|
|
29
|
-
/\bgit\s+(?:diff|log|status|show|rev-parse|ls-files|branch\s+--show-current)\b/,
|
|
30
35
|
];
|
|
31
36
|
|
|
32
|
-
const
|
|
37
|
+
const PYTHON_RISK_PATTERNS: RiskRule[] = [
|
|
33
38
|
// Irreversible: cannot be undone by a local revert.
|
|
34
39
|
{
|
|
35
40
|
risk: 'irreversible',
|
|
36
41
|
pattern: /\b(?:uv|poetry|flit|hatch)\s+publish\b|\btwine\s+upload\b/,
|
|
37
42
|
reason: 'Publishing to a package index is public and cannot be retracted.',
|
|
38
43
|
},
|
|
39
|
-
{
|
|
40
|
-
risk: 'irreversible',
|
|
41
|
-
pattern: /\bgit\s+push\b[^&|;]*(?:--force(?:-with-lease)?|-f\b)/,
|
|
42
|
-
reason: 'Force pushing rewrites shared remote history.',
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
risk: 'irreversible',
|
|
46
|
-
pattern: /\bgit\s+(?:reset\s+--hard|clean\b[^&|;]*-[a-z]*f)/,
|
|
47
|
-
reason: 'Hard reset or clean discards uncommitted work permanently.',
|
|
48
|
-
},
|
|
49
44
|
{
|
|
50
45
|
risk: 'irreversible',
|
|
51
46
|
pattern: /\b(?:conda|mamba)\s+env\s+remove\b|\bconda\s+remove\b[^&|;]*--all\b/,
|
|
52
47
|
reason: 'Removing an environment destroys installed state.',
|
|
53
48
|
},
|
|
54
|
-
{
|
|
55
|
-
risk: 'irreversible',
|
|
56
|
-
pattern: /\brm\b[^&|;]*-[a-z]*[rf][a-z]*/,
|
|
57
|
-
reason: 'Recursive or forced deletion is not recoverable.',
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
risk: 'irreversible',
|
|
61
|
-
pattern:
|
|
62
|
-
/\b(?:drop|truncate)\s+(?:table|database|schema)\b|\bdelete\s+from\b(?![^&|;]*\bwhere\b)/i,
|
|
63
|
-
reason: 'Destructive SQL without a narrowing predicate.',
|
|
64
|
-
},
|
|
65
49
|
{
|
|
66
50
|
risk: 'irreversible',
|
|
67
51
|
pattern: /\b(?:alembic|manage\.py)\b[^&|;]*(?:downgrade|\bzero\b)/,
|
|
68
52
|
reason: 'Reversing a database migration can drop data.',
|
|
69
53
|
},
|
|
70
|
-
{
|
|
71
|
-
risk: 'irreversible',
|
|
72
|
-
pattern: /\bdocker\s+(?:system|volume|image)\s+(?:prune|rm)\b/,
|
|
73
|
-
reason: 'Docker prune removes volumes or images outside the project.',
|
|
74
|
-
},
|
|
75
54
|
|
|
76
55
|
// Mutating: changes project, environment, or remote state but is recoverable.
|
|
77
56
|
{
|
|
@@ -104,78 +83,27 @@ const RISK_PATTERNS: RiskPattern[] = [
|
|
|
104
83
|
pattern: /\bpre-commit\s+(?:install|autoupdate|run|clean)\b/,
|
|
105
84
|
reason: 'Rewrites hook configuration or working tree files.',
|
|
106
85
|
},
|
|
107
|
-
{
|
|
108
|
-
risk: 'mutating',
|
|
109
|
-
pattern: /\bgit\s+(?:commit|add|checkout|switch|restore|stash|merge|rebase|push|tag)\b/,
|
|
110
|
-
reason: 'Changes repository or remote state.',
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
risk: 'mutating',
|
|
114
|
-
pattern: /\b(?:rm|mv|chmod|chown|truncate)\b/,
|
|
115
|
-
reason: 'Changes files on disk.',
|
|
116
|
-
},
|
|
117
86
|
];
|
|
118
87
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
.map((segment) => segment.trim())
|
|
124
|
-
.filter((segment) => segment.length > 0);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function classifySegment(segment: string): { risk: CommandRisk; reason?: string } {
|
|
128
|
-
if (SAFE_OVERRIDES.some((pattern) => pattern.test(segment))) return { risk: 'read' };
|
|
129
|
-
for (const entry of RISK_PATTERNS) {
|
|
130
|
-
if (entry.pattern.test(segment)) return { risk: entry.risk, reason: entry.reason };
|
|
131
|
-
}
|
|
132
|
-
return { risk: 'read' };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export interface CommandClassification {
|
|
136
|
-
risk: CommandRisk;
|
|
137
|
-
reasons: { segment: string; risk: CommandRisk; reason: string }[];
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** `curl ... | sh` cannot be seen after segment splitting, so it is matched first. */
|
|
141
|
-
const PIPE_TO_SHELL = /\b(?:curl|wget)\b[^;&\n]*\|\s*(?:sudo\s+)?(?:ba|z|k)?sh\b/;
|
|
88
|
+
const PYTHON_RULES: Partial<SafetyRules> = {
|
|
89
|
+
safeOverrides: PYTHON_SAFE_OVERRIDES,
|
|
90
|
+
patterns: PYTHON_RISK_PATTERNS,
|
|
91
|
+
};
|
|
142
92
|
|
|
143
93
|
/**
|
|
144
94
|
* Classify a shell command by the highest risk of its segments. Used to warn
|
|
145
95
|
* before a tool runs something that cannot be undone, and to gate this
|
|
146
96
|
* package's own environment-modifying commands behind explicit opt-in.
|
|
147
97
|
*/
|
|
148
|
-
export function classifyCommand(command: string): CommandClassification {
|
|
149
|
-
|
|
150
|
-
if (piped) {
|
|
151
|
-
return {
|
|
152
|
-
risk: 'irreversible',
|
|
153
|
-
reasons: [
|
|
154
|
-
{
|
|
155
|
-
segment: piped[0].trim(),
|
|
156
|
-
risk: 'irreversible',
|
|
157
|
-
reason: 'Piping a download into a shell runs unreviewed code.',
|
|
158
|
-
},
|
|
159
|
-
],
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
const reasons: CommandClassification['reasons'] = [];
|
|
163
|
-
let risk: CommandRisk = 'read';
|
|
164
|
-
for (const segment of splitCommandSegments(command)) {
|
|
165
|
-
const classified = classifySegment(segment);
|
|
166
|
-
if (RISK_ORDER[classified.risk] > RISK_ORDER[risk]) risk = classified.risk;
|
|
167
|
-
if (classified.risk !== 'read' && classified.reason) {
|
|
168
|
-
reasons.push({ segment, risk: classified.risk, reason: classified.reason });
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return { risk, reasons };
|
|
98
|
+
export function classifyCommand(command: string): import('pi-helper-core').CommandClassification {
|
|
99
|
+
return coreClassifyCommand(command, PYTHON_RULES);
|
|
172
100
|
}
|
|
173
101
|
|
|
174
102
|
export function isMutatingCommand(command: string): boolean {
|
|
175
|
-
return
|
|
103
|
+
return coreIsMutatingCommand(command, PYTHON_RULES);
|
|
176
104
|
}
|
|
177
105
|
|
|
178
106
|
/** Commands that this package runs itself always carry a known risk class. */
|
|
179
107
|
export function riskOf(args: string[]): CommandRisk {
|
|
180
|
-
return
|
|
108
|
+
return coreRiskOf(args, PYTHON_RULES);
|
|
181
109
|
}
|