@aliou/pi-guardrails 0.5.4 → 0.6.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/array-editor.ts DELETED
@@ -1,213 +0,0 @@
1
- import type { Component, SettingsListTheme } from "@mariozechner/pi-tui";
2
- import {
3
- Input,
4
- Key,
5
- matchesKey,
6
- truncateToWidth,
7
- visibleWidth,
8
- } from "@mariozechner/pi-tui";
9
-
10
- /**
11
- * A submenu component for editing string arrays inside a SettingsList.
12
- *
13
- * Modes:
14
- * - list: navigate items, delete with 'd', add with 'a', edit with 'e'/Enter
15
- * - add: text input for new item, confirm with Enter, cancel with Escape
16
- * - edit: text input pre-filled with current value, Enter saves, Escape cancels
17
- */
18
-
19
- export interface ArrayEditorOptions {
20
- label: string;
21
- items: string[];
22
- theme: SettingsListTheme;
23
- onSave: (items: string[]) => void;
24
- onDone: () => void;
25
- /** Max visible items before scrolling */
26
- maxVisible?: number;
27
- }
28
-
29
- export class ArrayEditor implements Component {
30
- private items: string[];
31
- private label: string;
32
- private theme: SettingsListTheme;
33
- private onSave: (items: string[]) => void;
34
- private onDone: () => void;
35
- private selectedIndex = 0;
36
- private maxVisible: number;
37
- private mode: "list" | "add" | "edit" = "list";
38
- private input: Input;
39
- private editIndex = -1;
40
-
41
- constructor(options: ArrayEditorOptions) {
42
- this.items = [...options.items];
43
- this.label = options.label;
44
- this.theme = options.theme;
45
- this.onSave = options.onSave;
46
- this.onDone = options.onDone;
47
- this.maxVisible = options.maxVisible ?? 10;
48
- this.input = new Input();
49
- this.input.onSubmit = (value: string) => {
50
- if (this.mode === "edit") {
51
- this.submitEdit(value);
52
- } else {
53
- this.submitAdd(value);
54
- }
55
- };
56
- this.input.onEscape = () => {
57
- this.mode = "list";
58
- this.editIndex = -1;
59
- };
60
- }
61
-
62
- private submitAdd(value: string) {
63
- const trimmed = value.trim();
64
- if (!trimmed) {
65
- this.mode = "list";
66
- return;
67
- }
68
- this.items.push(trimmed);
69
- this.selectedIndex = this.items.length - 1;
70
- this.save();
71
- this.mode = "list";
72
- this.input.setValue("");
73
- }
74
-
75
- private submitEdit(value: string) {
76
- const trimmed = value.trim();
77
- if (!trimmed) {
78
- // Empty value = cancel edit
79
- this.mode = "list";
80
- this.editIndex = -1;
81
- return;
82
- }
83
- this.items[this.editIndex] = trimmed;
84
- this.save();
85
- this.mode = "list";
86
- this.editIndex = -1;
87
- this.input.setValue("");
88
- }
89
-
90
- private deleteSelected() {
91
- if (this.items.length === 0) return;
92
- this.items.splice(this.selectedIndex, 1);
93
- if (this.selectedIndex >= this.items.length) {
94
- this.selectedIndex = Math.max(0, this.items.length - 1);
95
- }
96
- this.save();
97
- }
98
-
99
- private startEdit() {
100
- if (this.items.length === 0) return;
101
- this.editIndex = this.selectedIndex;
102
- this.mode = "edit";
103
- this.input.setValue(this.items[this.selectedIndex] as string);
104
- }
105
-
106
- private save() {
107
- this.onSave([...this.items]);
108
- }
109
-
110
- invalidate() {}
111
-
112
- render(width: number): string[] {
113
- const lines: string[] = [];
114
-
115
- // Header
116
- lines.push(this.theme.label(` ${this.label}`, true));
117
- lines.push("");
118
-
119
- if (this.mode === "add" || this.mode === "edit") {
120
- return [...lines, ...this.renderInputMode(width)];
121
- }
122
-
123
- return [...lines, ...this.renderListMode(width)];
124
- }
125
-
126
- private renderListMode(width: number): string[] {
127
- const lines: string[] = [];
128
-
129
- if (this.items.length === 0) {
130
- lines.push(this.theme.hint(" (empty)"));
131
- } else {
132
- const startIndex = Math.max(
133
- 0,
134
- Math.min(
135
- this.selectedIndex - Math.floor(this.maxVisible / 2),
136
- this.items.length - this.maxVisible,
137
- ),
138
- );
139
- const endIndex = Math.min(
140
- startIndex + this.maxVisible,
141
- this.items.length,
142
- );
143
-
144
- for (let i = startIndex; i < endIndex; i++) {
145
- const item = this.items[i];
146
- if (!item) continue;
147
- const isSelected = i === this.selectedIndex;
148
- const prefix = isSelected ? this.theme.cursor : " ";
149
- const prefixWidth = visibleWidth(prefix);
150
- const maxItemWidth = width - prefixWidth - 2;
151
- const text = this.theme.value(
152
- truncateToWidth(item, maxItemWidth, ""),
153
- isSelected,
154
- );
155
- lines.push(prefix + text);
156
- }
157
-
158
- if (startIndex > 0 || endIndex < this.items.length) {
159
- lines.push(
160
- this.theme.hint(` (${this.selectedIndex + 1}/${this.items.length})`),
161
- );
162
- }
163
- }
164
-
165
- lines.push("");
166
- lines.push(
167
- this.theme.hint(" a: add · e/Enter: edit · d: delete · Esc: back"),
168
- );
169
-
170
- return lines;
171
- }
172
-
173
- private renderInputMode(width: number): string[] {
174
- const lines: string[] = [];
175
- const label = this.mode === "edit" ? " Edit item:" : " New item:";
176
- lines.push(this.theme.hint(label));
177
- lines.push(` ${this.input.render(width - 4).join("")}`);
178
- lines.push("");
179
- lines.push(this.theme.hint(" Enter: confirm · Esc: cancel"));
180
- return lines;
181
- }
182
-
183
- handleInput(data: string) {
184
- if (this.mode === "add" || this.mode === "edit") {
185
- this.input.handleInput(data);
186
- return;
187
- }
188
-
189
- // List mode
190
- if (matchesKey(data, Key.up) || data === "k") {
191
- if (this.items.length === 0) return;
192
- this.selectedIndex =
193
- this.selectedIndex === 0
194
- ? this.items.length - 1
195
- : this.selectedIndex - 1;
196
- } else if (matchesKey(data, Key.down) || data === "j") {
197
- if (this.items.length === 0) return;
198
- this.selectedIndex =
199
- this.selectedIndex === this.items.length - 1
200
- ? 0
201
- : this.selectedIndex + 1;
202
- } else if (data === "a" || data === "A") {
203
- this.mode = "add";
204
- this.input.setValue("");
205
- } else if (data === "e" || data === "E" || matchesKey(data, Key.enter)) {
206
- this.startEdit();
207
- } else if (data === "d" || data === "D") {
208
- this.deleteSelected();
209
- } else if (matchesKey(data, Key.escape)) {
210
- this.onDone();
211
- }
212
- }
213
- }
package/config-schema.ts DELETED
@@ -1,64 +0,0 @@
1
- /**
2
- * Configuration schema for the guardrails extension.
3
- *
4
- * GuardrailsConfig is the user-facing schema (all fields optional).
5
- * ResolvedConfig is the internal schema (all fields required, defaults applied).
6
- */
7
-
8
- export interface GuardrailsConfig {
9
- enabled?: boolean;
10
- features?: {
11
- preventBrew?: boolean;
12
- preventPython?: boolean;
13
- protectEnvFiles?: boolean;
14
- permissionGate?: boolean;
15
- enforcePackageManager?: boolean;
16
- };
17
- packageManager?: {
18
- selected?: "bun" | "pnpm" | "npm";
19
- };
20
- envFiles?: {
21
- protectedPatterns?: string[];
22
- allowedPatterns?: string[];
23
- protectedDirectories?: string[];
24
- protectedTools?: string[];
25
- onlyBlockIfExists?: boolean;
26
- blockMessage?: string;
27
- };
28
- permissionGate?: {
29
- patterns?: Array<{ pattern: string; description: string }>;
30
- /** If set, replaces the default patterns entirely. */
31
- customPatterns?: Array<{ pattern: string; description: string }>;
32
- requireConfirmation?: boolean;
33
- allowedPatterns?: string[];
34
- autoDenyPatterns?: string[];
35
- };
36
- }
37
-
38
- export interface ResolvedConfig {
39
- enabled: boolean;
40
- features: {
41
- preventBrew: boolean;
42
- preventPython: boolean;
43
- protectEnvFiles: boolean;
44
- permissionGate: boolean;
45
- enforcePackageManager: boolean;
46
- };
47
- packageManager: {
48
- selected: "bun" | "pnpm" | "npm";
49
- };
50
- envFiles: {
51
- protectedPatterns: string[];
52
- allowedPatterns: string[];
53
- protectedDirectories: string[];
54
- protectedTools: string[];
55
- onlyBlockIfExists: boolean;
56
- blockMessage: string;
57
- };
58
- permissionGate: {
59
- patterns: Array<{ pattern: string; description: string }>;
60
- requireConfirmation: boolean;
61
- allowedPatterns: string[];
62
- autoDenyPatterns: string[];
63
- };
64
- }
@@ -1,96 +0,0 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import type { ResolvedConfig } from "../config-schema";
3
- import { emitBlocked } from "../events";
4
-
5
- /**
6
- * Enforces using a specific Node package manager (bun, pnpm, or npm).
7
- * Blocks commands using non-selected package managers.
8
- */
9
-
10
- const BUN_PATTERN = /\bbun\b/;
11
- const PNPM_PATTERN = /\bpnpm\b/;
12
- const NPM_PATTERN = /\bnpm\b/;
13
-
14
- type PackageManager = "bun" | "pnpm" | "npm";
15
-
16
- interface ManagerInfo {
17
- pattern: RegExp;
18
- name: string;
19
- installCmd: string;
20
- addCmd: string;
21
- runCmd: string;
22
- }
23
-
24
- const MANAGER_INFO: Record<PackageManager, ManagerInfo> = {
25
- bun: {
26
- pattern: BUN_PATTERN,
27
- name: "bun",
28
- installCmd: "bun install",
29
- addCmd: "bun add <package>",
30
- runCmd: "bun run <script>",
31
- },
32
- pnpm: {
33
- pattern: PNPM_PATTERN,
34
- name: "pnpm",
35
- installCmd: "pnpm install",
36
- addCmd: "pnpm add <package>",
37
- runCmd: "pnpm run <script>",
38
- },
39
- npm: {
40
- pattern: NPM_PATTERN,
41
- name: "npm",
42
- installCmd: "npm install",
43
- addCmd: "npm install <package>",
44
- runCmd: "npm run <script>",
45
- },
46
- };
47
-
48
- export function setupEnforcePackageManagerHook(
49
- pi: ExtensionAPI,
50
- config: ResolvedConfig,
51
- ) {
52
- if (!config.features.enforcePackageManager) return;
53
-
54
- const selectedManager = config.packageManager.selected;
55
- const selected = MANAGER_INFO[selectedManager];
56
-
57
- // Get all managers that should be blocked (all except the selected one)
58
- const blockedManagers = (
59
- Object.keys(MANAGER_INFO) as PackageManager[]
60
- ).filter((m) => m !== selectedManager);
61
-
62
- pi.on("tool_call", async (event, ctx) => {
63
- if (event.toolName !== "bash") return;
64
-
65
- const command = String(event.input.command ?? "");
66
-
67
- for (const blockedManager of blockedManagers) {
68
- const blocked = MANAGER_INFO[blockedManager];
69
-
70
- if (blocked.pattern.test(command)) {
71
- ctx.ui.notify(
72
- `Blocked ${blocked.name} command. Use ${selected.name} instead.`,
73
- "warning",
74
- );
75
-
76
- const reason =
77
- `This project uses ${selected.name} as its package manager. ` +
78
- `Use ${selected.name} instead of ${blocked.name}. ` +
79
- `Run \`${selected.installCmd}\` to install dependencies, ` +
80
- `\`${selected.addCmd}\` to add packages, ` +
81
- `and \`${selected.runCmd}\` to run scripts.`;
82
-
83
- emitBlocked(pi, {
84
- feature: "enforcePackageManager",
85
- toolName: "bash",
86
- input: event.input,
87
- reason,
88
- });
89
-
90
- return { block: true, reason };
91
- }
92
- }
93
-
94
- return;
95
- });
96
- }
@@ -1,41 +0,0 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import type { ResolvedConfig } from "../config-schema";
3
- import { emitBlocked } from "../events";
4
-
5
- /**
6
- * Blocks all brew commands. Homebrew is not installed on this machine.
7
- */
8
-
9
- const BREW_PATTERN = /\bbrew\b/;
10
-
11
- export function setupPreventBrewHook(pi: ExtensionAPI, config: ResolvedConfig) {
12
- if (!config.features.preventBrew) return;
13
-
14
- pi.on("tool_call", async (event, ctx) => {
15
- if (event.toolName !== "bash") return;
16
-
17
- const command = String(event.input.command ?? "");
18
-
19
- if (BREW_PATTERN.test(command)) {
20
- ctx.ui.notify(
21
- "Blocked brew command. Homebrew is not installed.",
22
- "warning",
23
- );
24
-
25
- const reason =
26
- "Homebrew is not installed on this machine. " +
27
- "Use Nix for package management instead. " +
28
- "Run packages via nix-shell or add them to the project's Nix configuration.";
29
-
30
- emitBlocked(pi, {
31
- feature: "preventBrew",
32
- toolName: "bash",
33
- input: event.input,
34
- reason,
35
- });
36
-
37
- return { block: true, reason };
38
- }
39
- return;
40
- });
41
- }
@@ -1,45 +0,0 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import type { ResolvedConfig } from "../config-schema";
3
- import { emitBlocked } from "../events";
4
-
5
- /**
6
- * Blocks all Python-related commands including python, python3, pip, poetry, etc.
7
- * Use uv for Python package management instead.
8
- */
9
-
10
- const PYTHON_PATTERN =
11
- /\b(python|python3|pip|pip3|poetry|pyenv|virtualenv|venv)\b/;
12
-
13
- export function setupPreventPythonHook(
14
- pi: ExtensionAPI,
15
- config: ResolvedConfig,
16
- ) {
17
- if (!config.features.preventPython) return;
18
-
19
- pi.on("tool_call", async (event, ctx) => {
20
- if (event.toolName !== "bash") return;
21
-
22
- const command = String(event.input.command ?? "");
23
-
24
- if (PYTHON_PATTERN.test(command)) {
25
- ctx.ui.notify("Blocked Python command. Use uv instead.", "warning");
26
-
27
- const reason =
28
- "Python is not available globally on this machine. " +
29
- "Use uv for Python package management instead. " +
30
- "Run `uv init` to create a new Python project, " +
31
- "or `uv run python` to run Python scripts. " +
32
- "Use `uv add` to install packages (replaces pip/poetry).";
33
-
34
- emitBlocked(pi, {
35
- feature: "preventPython",
36
- toolName: "bash",
37
- input: event.input,
38
- reason,
39
- });
40
-
41
- return { block: true, reason };
42
- }
43
- return;
44
- });
45
- }