@aliou/pi-guardrails 0.11.2 → 0.12.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/README.md +72 -167
- package/extensions/guardrails/commands/examples/index.ts +520 -0
- package/extensions/guardrails/commands/onboarding/config.ts +54 -0
- package/{src/commands/onboarding-command.ts → extensions/guardrails/commands/onboarding/index.ts} +5 -31
- package/extensions/guardrails/commands/settings/add-rule-wizard.ts +267 -0
- package/extensions/guardrails/commands/settings/examples.ts +399 -0
- package/extensions/guardrails/commands/settings/index.ts +596 -0
- package/extensions/guardrails/commands/settings/path-list-editor.ts +158 -0
- package/extensions/guardrails/commands/settings/scope-picker-submenu.ts +69 -0
- package/extensions/guardrails/commands/settings/utils.ts +108 -0
- package/extensions/guardrails/components/onboarding-choice-step.ts +140 -0
- package/extensions/guardrails/components/onboarding-finish-step.ts +50 -0
- package/extensions/guardrails/components/onboarding-intro-step.ts +30 -0
- package/extensions/guardrails/components/onboarding-types.ts +10 -0
- package/extensions/guardrails/components/onboarding-wizard.ts +116 -0
- package/{src → extensions/guardrails}/components/pattern-editor.ts +11 -10
- package/extensions/guardrails/index.ts +106 -0
- package/extensions/guardrails/rules.test.ts +107 -0
- package/extensions/guardrails/rules.ts +119 -0
- package/extensions/guardrails/targets.test.ts +44 -0
- package/extensions/guardrails/targets.ts +66 -0
- package/extensions/path-access/grants.test.ts +47 -0
- package/extensions/path-access/grants.ts +68 -0
- package/extensions/path-access/index.ts +143 -0
- package/extensions/path-access/prompt.ts +196 -0
- package/extensions/path-access/rules.test.ts +46 -0
- package/extensions/path-access/rules.ts +37 -0
- package/extensions/path-access/targets.test.ts +40 -0
- package/extensions/path-access/targets.ts +19 -0
- package/extensions/permission-gate/grants.ts +21 -0
- package/extensions/permission-gate/index.ts +122 -0
- package/extensions/permission-gate/prompt.ts +222 -0
- package/extensions/permission-gate/rules.test.ts +132 -0
- package/extensions/permission-gate/rules.ts +72 -0
- package/package.json +18 -20
- package/schema.json +286 -0
- package/src/core/check.test.ts +169 -0
- package/src/core/check.ts +38 -0
- package/src/{hooks/permission-gate/dangerous-commands.test.ts → core/commands/dangerous.test.ts} +134 -2
- package/src/{hooks/permission-gate/dangerous-commands.ts → core/commands/dangerous.ts} +119 -1
- package/src/core/commands/index.ts +15 -0
- package/src/core/index.ts +13 -0
- package/src/{utils/path-access.test.ts → core/paths/access.test.ts} +1 -5
- package/src/core/paths/index.ts +14 -0
- package/src/core/shell/command-args.test.ts +142 -0
- package/src/{utils → core/shell}/command-args.ts +71 -0
- package/src/core/shell/index.ts +2 -0
- package/src/core/types.ts +55 -0
- package/src/shared/config/defaults.ts +118 -0
- package/src/shared/config/index.ts +17 -0
- package/src/shared/config/loader.ts +64 -0
- package/src/shared/config/migration/001-v0-format-upgrade.ts +107 -0
- package/src/shared/config/migration/002-strip-toolchain-fields.ts +39 -0
- package/src/shared/config/migration/003-strip-command-explainer-fields.ts +42 -0
- package/src/shared/config/migration/004-env-files-to-policies.ts +87 -0
- package/src/shared/config/migration/005-normalize-allowed-paths.ts +43 -0
- package/src/shared/config/migration/006-apply-builtin-defaults.ts +19 -0
- package/src/shared/config/migration/007-mark-onboarding-done.ts +25 -0
- package/src/shared/config/migration/index.ts +44 -0
- package/src/shared/config/migration/version.ts +7 -0
- package/src/shared/config/types.ts +141 -0
- package/src/shared/events.ts +100 -0
- package/src/shared/index.ts +6 -0
- package/src/shared/matching.test.ts +86 -0
- package/src/{utils → shared}/matching.ts +4 -4
- package/src/{utils → shared/paths}/bash-paths.test.ts +32 -2
- package/src/{utils → shared/paths}/bash-paths.ts +4 -4
- package/src/shared/paths/index.ts +1 -0
- package/src/shared/warnings.ts +17 -0
- package/docs/defaults.md +0 -140
- package/docs/examples.md +0 -170
- package/src/commands/onboarding.ts +0 -390
- package/src/commands/settings-command.ts +0 -1616
- package/src/config.ts +0 -392
- package/src/hooks/index.ts +0 -11
- package/src/hooks/path-access.ts +0 -395
- package/src/hooks/permission-gate/index.test.ts +0 -332
- package/src/hooks/permission-gate/index.ts +0 -595
- package/src/hooks/policies.ts +0 -322
- package/src/index.ts +0 -96
- package/src/lib/executor.ts +0 -280
- package/src/lib/index.ts +0 -16
- package/src/lib/model-resolver.ts +0 -47
- package/src/lib/timing.ts +0 -42
- package/src/lib/types.ts +0 -115
- package/src/utils/command-args.test.ts +0 -83
- package/src/utils/events.ts +0 -32
- package/src/utils/migration.test.ts +0 -58
- package/src/utils/migration.ts +0 -340
- package/src/utils/warnings.ts +0 -7
- /package/src/{utils/path-access.ts → core/paths/access.ts} +0 -0
- /package/src/{utils → core/paths}/path.test.ts +0 -0
- /package/src/{utils → core/paths}/path.ts +0 -0
- /package/src/{utils/shell-utils.ts → core/shell/ast.ts} +0 -0
- /package/src/{utils/glob-expander.ts → shared/glob.ts} +0 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Component,
|
|
3
|
+
Input,
|
|
4
|
+
Key,
|
|
5
|
+
matchesKey,
|
|
6
|
+
type SettingsListTheme,
|
|
7
|
+
} from "@earendil-works/pi-tui";
|
|
8
|
+
|
|
9
|
+
export class PathListEditor implements Component {
|
|
10
|
+
private readonly input = new Input();
|
|
11
|
+
private items: string[];
|
|
12
|
+
private selectedIndex = 0;
|
|
13
|
+
private mode: "list" | "add" | "edit" = "list";
|
|
14
|
+
private editIndex = -1;
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
private readonly options: {
|
|
18
|
+
label: string;
|
|
19
|
+
items: string[];
|
|
20
|
+
theme: SettingsListTheme;
|
|
21
|
+
onSave: (items: string[]) => void;
|
|
22
|
+
onDone: () => void;
|
|
23
|
+
maxVisible?: number;
|
|
24
|
+
},
|
|
25
|
+
) {
|
|
26
|
+
this.items = [...options.items];
|
|
27
|
+
this.input.onSubmit = () => this.submit();
|
|
28
|
+
this.input.onEscape = () => this.cancel();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
invalidate() {}
|
|
32
|
+
|
|
33
|
+
render(width: number): string[] {
|
|
34
|
+
const lines = [
|
|
35
|
+
this.options.theme.label(` ${this.options.label}`, true),
|
|
36
|
+
"",
|
|
37
|
+
];
|
|
38
|
+
if (this.mode === "add" || this.mode === "edit") {
|
|
39
|
+
lines.push(
|
|
40
|
+
this.options.theme.hint(
|
|
41
|
+
this.mode === "edit" ? " Edit path:" : " New path:",
|
|
42
|
+
),
|
|
43
|
+
"",
|
|
44
|
+
...this.input.render(Math.max(1, width - 4)).map((line) => ` ${line}`),
|
|
45
|
+
"",
|
|
46
|
+
this.options.theme.hint(" Enter: save · Esc: cancel"),
|
|
47
|
+
);
|
|
48
|
+
return lines;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (this.items.length === 0) {
|
|
52
|
+
lines.push(this.options.theme.hint(" (empty)"));
|
|
53
|
+
} else {
|
|
54
|
+
const maxVisible = this.options.maxVisible ?? 10;
|
|
55
|
+
const startIndex = Math.max(
|
|
56
|
+
0,
|
|
57
|
+
Math.min(
|
|
58
|
+
this.selectedIndex - Math.floor(maxVisible / 2),
|
|
59
|
+
this.items.length - maxVisible,
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
const endIndex = Math.min(startIndex + maxVisible, this.items.length);
|
|
63
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
64
|
+
const item = this.items[i];
|
|
65
|
+
if (!item) continue;
|
|
66
|
+
const isSelected = i === this.selectedIndex;
|
|
67
|
+
const prefix = isSelected ? this.options.theme.cursor : " ";
|
|
68
|
+
lines.push(prefix + this.options.theme.value(item, isSelected));
|
|
69
|
+
}
|
|
70
|
+
if (startIndex > 0 || endIndex < this.items.length) {
|
|
71
|
+
lines.push(
|
|
72
|
+
this.options.theme.hint(
|
|
73
|
+
` (${this.selectedIndex + 1}/${this.items.length})`,
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
lines.push("");
|
|
80
|
+
lines.push(
|
|
81
|
+
this.options.theme.hint(
|
|
82
|
+
" a: add · e/Enter: edit · d: delete · Esc: back",
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
return lines;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
handleInput(data: string): void {
|
|
89
|
+
if (this.mode === "add" || this.mode === "edit") {
|
|
90
|
+
this.input.handleInput(data);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (matchesKey(data, Key.up) || data === "k") {
|
|
95
|
+
if (this.items.length === 0) return;
|
|
96
|
+
this.selectedIndex =
|
|
97
|
+
this.selectedIndex === 0
|
|
98
|
+
? this.items.length - 1
|
|
99
|
+
: this.selectedIndex - 1;
|
|
100
|
+
} else if (matchesKey(data, Key.down) || data === "j") {
|
|
101
|
+
if (this.items.length === 0) return;
|
|
102
|
+
this.selectedIndex =
|
|
103
|
+
this.selectedIndex === this.items.length - 1
|
|
104
|
+
? 0
|
|
105
|
+
: this.selectedIndex + 1;
|
|
106
|
+
} else if (data === "a" || data === "A") {
|
|
107
|
+
this.mode = "add";
|
|
108
|
+
this.input.setValue("");
|
|
109
|
+
} else if (data === "e" || data === "E" || matchesKey(data, Key.enter)) {
|
|
110
|
+
this.startEdit();
|
|
111
|
+
} else if (data === "d" || data === "D") {
|
|
112
|
+
this.deleteSelected();
|
|
113
|
+
} else if (matchesKey(data, Key.escape)) {
|
|
114
|
+
this.options.onDone();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private startEdit(): void {
|
|
119
|
+
const item = this.items[this.selectedIndex];
|
|
120
|
+
if (!item) return;
|
|
121
|
+
this.mode = "edit";
|
|
122
|
+
this.editIndex = this.selectedIndex;
|
|
123
|
+
this.input.setValue(item);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private submit(): void {
|
|
127
|
+
const path = this.input.getValue().trim();
|
|
128
|
+
if (!path) {
|
|
129
|
+
this.cancel();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (this.mode === "edit") {
|
|
134
|
+
this.items[this.editIndex] = path;
|
|
135
|
+
} else {
|
|
136
|
+
this.items.push(path);
|
|
137
|
+
this.selectedIndex = this.items.length - 1;
|
|
138
|
+
}
|
|
139
|
+
this.items = [...new Set(this.items)];
|
|
140
|
+
this.options.onSave([...this.items]);
|
|
141
|
+
this.cancel();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private deleteSelected(): void {
|
|
145
|
+
if (this.items.length === 0) return;
|
|
146
|
+
this.items.splice(this.selectedIndex, 1);
|
|
147
|
+
if (this.selectedIndex >= this.items.length) {
|
|
148
|
+
this.selectedIndex = Math.max(0, this.items.length - 1);
|
|
149
|
+
}
|
|
150
|
+
this.options.onSave([...this.items]);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private cancel(): void {
|
|
154
|
+
this.mode = "list";
|
|
155
|
+
this.editIndex = -1;
|
|
156
|
+
this.input.setValue("");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Component,
|
|
3
|
+
Key,
|
|
4
|
+
matchesKey,
|
|
5
|
+
type SettingsListTheme,
|
|
6
|
+
} from "@earendil-works/pi-tui";
|
|
7
|
+
|
|
8
|
+
export class ScopePickerSubmenu implements Component {
|
|
9
|
+
private selectedIndex = 0;
|
|
10
|
+
|
|
11
|
+
constructor(
|
|
12
|
+
private readonly theme: SettingsListTheme,
|
|
13
|
+
private readonly scopes: Array<"global" | "local" | "memory">,
|
|
14
|
+
private readonly onSelect: (scope: "global" | "local" | "memory") => void,
|
|
15
|
+
private readonly onDone: (value?: string) => void,
|
|
16
|
+
) {}
|
|
17
|
+
|
|
18
|
+
invalidate() {}
|
|
19
|
+
|
|
20
|
+
render(_width: number): string[] {
|
|
21
|
+
const lines: string[] = [
|
|
22
|
+
this.theme.label(" Add example to scope", true),
|
|
23
|
+
"",
|
|
24
|
+
this.theme.hint(" Select target scope:"),
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < this.scopes.length; i++) {
|
|
28
|
+
const scope = this.scopes[i];
|
|
29
|
+
if (!scope) continue;
|
|
30
|
+
const isSelected = i === this.selectedIndex;
|
|
31
|
+
const prefix = isSelected ? this.theme.cursor : " ";
|
|
32
|
+
lines.push(`${prefix}${this.theme.value(scope, isSelected)}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
lines.push("");
|
|
36
|
+
lines.push(this.theme.hint(" Enter: apply · Esc: back"));
|
|
37
|
+
return lines;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
handleInput(data: string): void {
|
|
41
|
+
if (matchesKey(data, Key.up) || data === "k") {
|
|
42
|
+
this.selectedIndex =
|
|
43
|
+
this.selectedIndex === 0
|
|
44
|
+
? this.scopes.length - 1
|
|
45
|
+
: this.selectedIndex - 1;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (matchesKey(data, Key.down) || data === "j") {
|
|
50
|
+
this.selectedIndex =
|
|
51
|
+
this.selectedIndex === this.scopes.length - 1
|
|
52
|
+
? 0
|
|
53
|
+
: this.selectedIndex + 1;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (matchesKey(data, Key.enter)) {
|
|
58
|
+
const scope = this.scopes[this.selectedIndex];
|
|
59
|
+
if (!scope) return;
|
|
60
|
+
this.onSelect(scope);
|
|
61
|
+
this.onDone(`applied to ${scope}`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (matchesKey(data, Key.escape)) {
|
|
66
|
+
this.onDone();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { getNestedValue, setNestedValue } from "@aliou/pi-utils-settings";
|
|
2
|
+
import type {
|
|
3
|
+
GuardrailsConfig,
|
|
4
|
+
PatternConfig,
|
|
5
|
+
PolicyRule,
|
|
6
|
+
Protection,
|
|
7
|
+
} from "../../../../src/shared/config";
|
|
8
|
+
|
|
9
|
+
export interface NewPolicyRuleDraft {
|
|
10
|
+
name: string;
|
|
11
|
+
id: string;
|
|
12
|
+
protection: Protection;
|
|
13
|
+
patterns: PatternConfig[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function toKebabCase(input: string): string {
|
|
17
|
+
return input
|
|
18
|
+
.trim()
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
21
|
+
.replace(/^-+|-+$/g, "");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function countItems(config: GuardrailsConfig, id: string): string {
|
|
25
|
+
const val = (getNestedValue(config, id) as unknown[] | undefined) ?? [];
|
|
26
|
+
return `${val.length} items`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function setConfigValue(
|
|
30
|
+
config: GuardrailsConfig,
|
|
31
|
+
id: string,
|
|
32
|
+
value: unknown,
|
|
33
|
+
): GuardrailsConfig {
|
|
34
|
+
const updated = structuredClone(config);
|
|
35
|
+
setNestedValue(updated, id, value);
|
|
36
|
+
return updated;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getPolicyRules(config: GuardrailsConfig): PolicyRule[] {
|
|
40
|
+
return config.policies?.rules?.map((rule) => ({ ...rule })) ?? [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function setPolicyRules(
|
|
44
|
+
config: GuardrailsConfig,
|
|
45
|
+
rules: PolicyRule[],
|
|
46
|
+
): GuardrailsConfig {
|
|
47
|
+
const updated = structuredClone(config);
|
|
48
|
+
updated.policies = {
|
|
49
|
+
...(updated.policies ?? {}),
|
|
50
|
+
rules,
|
|
51
|
+
};
|
|
52
|
+
return updated;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function updatePolicyRule(
|
|
56
|
+
config: GuardrailsConfig,
|
|
57
|
+
index: number,
|
|
58
|
+
updater: (rule: PolicyRule) => PolicyRule,
|
|
59
|
+
): GuardrailsConfig {
|
|
60
|
+
const rules = getPolicyRules(config);
|
|
61
|
+
const existing = rules[index];
|
|
62
|
+
if (!existing) return config;
|
|
63
|
+
rules[index] = updater(existing);
|
|
64
|
+
return setPolicyRules(config, rules);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function deletePolicyRule(
|
|
68
|
+
config: GuardrailsConfig,
|
|
69
|
+
index: number,
|
|
70
|
+
): GuardrailsConfig {
|
|
71
|
+
const rules = getPolicyRules(config);
|
|
72
|
+
if (!rules[index]) return config;
|
|
73
|
+
rules.splice(index, 1);
|
|
74
|
+
return setPolicyRules(config, rules);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function addPolicyRuleDraft(
|
|
78
|
+
config: GuardrailsConfig,
|
|
79
|
+
draft: NewPolicyRuleDraft,
|
|
80
|
+
): { config: GuardrailsConfig; index: number | null } {
|
|
81
|
+
const normalizedName = draft.name.trim();
|
|
82
|
+
if (!normalizedName || draft.patterns.length === 0) {
|
|
83
|
+
return { config, index: null };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const rules = getPolicyRules(config);
|
|
87
|
+
const baseId = toKebabCase(draft.id || normalizedName) || "policy";
|
|
88
|
+
const existingIds = new Set(rules.map((rule) => rule.id));
|
|
89
|
+
|
|
90
|
+
let id = baseId;
|
|
91
|
+
let i = 2;
|
|
92
|
+
while (existingIds.has(id)) {
|
|
93
|
+
id = `${baseId}-${i}`;
|
|
94
|
+
i++;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
rules.push({
|
|
98
|
+
id,
|
|
99
|
+
name: normalizedName,
|
|
100
|
+
description: "",
|
|
101
|
+
patterns: draft.patterns,
|
|
102
|
+
protection: draft.protection,
|
|
103
|
+
onlyIfExists: true,
|
|
104
|
+
enabled: true,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return { config: setPolicyRules(config, rules), index: rules.length - 1 };
|
|
108
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { getSettingsTheme, type SettingsTheme } from "@aliou/pi-utils-settings";
|
|
2
|
+
import { getMarkdownTheme, type Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
4
|
+
import { Box, Key, Markdown, matchesKey } from "@earendil-works/pi-tui";
|
|
5
|
+
import type { OnboardingState } from "./onboarding-types";
|
|
6
|
+
|
|
7
|
+
abstract class OnboardingChoiceStep implements Component {
|
|
8
|
+
private selectedIndex = 0;
|
|
9
|
+
private readonly settingsTheme: SettingsTheme;
|
|
10
|
+
|
|
11
|
+
protected constructor(
|
|
12
|
+
private readonly theme: Theme,
|
|
13
|
+
private readonly onSelect: (selectedIndex: number) => void,
|
|
14
|
+
) {
|
|
15
|
+
this.settingsTheme = getSettingsTheme(theme);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
invalidate() {}
|
|
19
|
+
|
|
20
|
+
protected abstract getTitle(): string;
|
|
21
|
+
protected abstract getOptions(): string[];
|
|
22
|
+
protected abstract getExplanations(): string[];
|
|
23
|
+
|
|
24
|
+
render(width: number): string[] {
|
|
25
|
+
const options = this.getOptions();
|
|
26
|
+
const explanations = this.getExplanations();
|
|
27
|
+
const lines: string[] = [` ${this.getTitle()}`, ""];
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i < options.length; i++) {
|
|
30
|
+
const option = options[i];
|
|
31
|
+
if (!option) continue;
|
|
32
|
+
const selected = i === this.selectedIndex;
|
|
33
|
+
const prefix = selected ? this.settingsTheme.cursor : " ";
|
|
34
|
+
const label = this.settingsTheme.value(` ${option}`, selected);
|
|
35
|
+
lines.push(`${prefix}${label}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
lines.push("");
|
|
39
|
+
|
|
40
|
+
const explanationBox = new Box(1, 0, (s: string) => s);
|
|
41
|
+
explanationBox.addChild(
|
|
42
|
+
new Markdown(
|
|
43
|
+
explanations[this.selectedIndex] ?? "",
|
|
44
|
+
0,
|
|
45
|
+
0,
|
|
46
|
+
getMarkdownTheme(),
|
|
47
|
+
{
|
|
48
|
+
color: (s: string) => this.theme.fg("text", s),
|
|
49
|
+
},
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
lines.push(...explanationBox.render(Math.max(1, width)));
|
|
54
|
+
return lines;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
handleInput(data: string): void {
|
|
58
|
+
if (matchesKey(data, Key.up) || data === "k") {
|
|
59
|
+
this.selectedIndex = this.selectedIndex === 0 ? 1 : 0;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (matchesKey(data, Key.down) || data === "j") {
|
|
63
|
+
this.selectedIndex = this.selectedIndex === 1 ? 0 : 1;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (matchesKey(data, Key.enter)) {
|
|
68
|
+
this.onSelect(this.selectedIndex);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class OnboardingDefaultsChoiceStep extends OnboardingChoiceStep {
|
|
74
|
+
constructor(state: OnboardingState, theme: Theme, onSelect: () => void) {
|
|
75
|
+
super(theme, (selectedIndex) => {
|
|
76
|
+
state.applyBuiltinDefaults = selectedIndex === 0;
|
|
77
|
+
onSelect();
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
protected getTitle(): string {
|
|
82
|
+
return "Pick how much built-in protection to start with.";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
protected getOptions(): string[] {
|
|
86
|
+
return ["Recommended defaults", "Minimal setup"];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
protected getExplanations(): string[] {
|
|
90
|
+
return [
|
|
91
|
+
[
|
|
92
|
+
"Use built-ins for common safety needs:",
|
|
93
|
+
"",
|
|
94
|
+
"- Protect secret files like `.env`, `.env.local`, `.env.production`, and `.dev.vars`",
|
|
95
|
+
"- Keep safe exceptions like `.env.example` and `*.sample.env`",
|
|
96
|
+
"- Require confirmation before running dangerous commands like `rm -rf`, `sudo`, and `dd of=`",
|
|
97
|
+
].join("\n"),
|
|
98
|
+
[
|
|
99
|
+
"Start with no built-in file policy defaults.",
|
|
100
|
+
"",
|
|
101
|
+
"- Configure your own policies in `/guardrails:settings`",
|
|
102
|
+
"- Browse policy and command examples in `/guardrails:settings`",
|
|
103
|
+
].join("\n"),
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export class OnboardingPathAccessStep extends OnboardingChoiceStep {
|
|
109
|
+
constructor(state: OnboardingState, theme: Theme, onSelect: () => void) {
|
|
110
|
+
super(theme, (selectedIndex) => {
|
|
111
|
+
state.pathAccessEnabled = selectedIndex === 0;
|
|
112
|
+
onSelect();
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
protected getTitle(): string {
|
|
117
|
+
return "Restrict access to your project directory?";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
protected getOptions(): string[] {
|
|
121
|
+
return ["Ask before accessing outside files", "No restrictions"];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
protected getExplanations(): string[] {
|
|
125
|
+
return [
|
|
126
|
+
[
|
|
127
|
+
"When enabled, guardrails will prompt you before the agent accesses files outside the current working directory.",
|
|
128
|
+
"",
|
|
129
|
+
"- You can grant access per-file or per-directory",
|
|
130
|
+
"- Grants can be session-only or permanent",
|
|
131
|
+
"- In non-interactive mode, outside access is blocked",
|
|
132
|
+
].join("\n"),
|
|
133
|
+
[
|
|
134
|
+
"The agent can access any path on your system without prompting.",
|
|
135
|
+
"",
|
|
136
|
+
"- You can enable path access later in `/guardrails:settings`",
|
|
137
|
+
].join("\n"),
|
|
138
|
+
];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
3
|
+
import { Key, Markdown, matchesKey } from "@earendil-works/pi-tui";
|
|
4
|
+
import type { OnboardingState } from "./onboarding-types";
|
|
5
|
+
|
|
6
|
+
export class OnboardingFinishStep implements Component {
|
|
7
|
+
private readonly recapMarkdown = new Markdown("", 2, 0, getMarkdownTheme());
|
|
8
|
+
|
|
9
|
+
constructor(
|
|
10
|
+
private readonly state: OnboardingState,
|
|
11
|
+
private readonly onFinish: () => void,
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
invalidate() {
|
|
15
|
+
this.recapMarkdown.invalidate();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
render(width: number): string[] {
|
|
19
|
+
const defaultsPart =
|
|
20
|
+
this.state.applyBuiltinDefaults === true
|
|
21
|
+
? [
|
|
22
|
+
"You selected **Recommended defaults**.",
|
|
23
|
+
"",
|
|
24
|
+
"Guardrails will start with built-in protection, including:",
|
|
25
|
+
"- secret files like `.env`, `.env.local`, `.env.production`, `.dev.vars`",
|
|
26
|
+
"- safe exceptions like `.env.example` and `*.sample.env`",
|
|
27
|
+
"- confirmation before running dangerous commands like `rm -rf`, `sudo`, `dd of=`",
|
|
28
|
+
].join("\n")
|
|
29
|
+
: [
|
|
30
|
+
"You selected **Minimal setup**.",
|
|
31
|
+
"",
|
|
32
|
+
"No built-in file policy defaults will be applied.",
|
|
33
|
+
"",
|
|
34
|
+
"You can configure policies later with `/guardrails:settings`.",
|
|
35
|
+
].join("\n");
|
|
36
|
+
|
|
37
|
+
const pathAccessPart = this.state.pathAccessEnabled
|
|
38
|
+
? "\n\n**Path access**: enabled (ask mode). The agent will prompt before accessing files outside the working directory."
|
|
39
|
+
: "\n\n**Path access**: disabled. No path restrictions.";
|
|
40
|
+
|
|
41
|
+
this.recapMarkdown.setText(defaultsPart + pathAccessPart);
|
|
42
|
+
return [...this.recapMarkdown.render(Math.max(1, width)), ""];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
handleInput(data: string): void {
|
|
46
|
+
if (matchesKey(data, Key.enter)) {
|
|
47
|
+
this.onFinish();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
2
|
+
import { Key, matchesKey, Text } from "@earendil-works/pi-tui";
|
|
3
|
+
|
|
4
|
+
export class OnboardingIntroStep implements Component {
|
|
5
|
+
private readonly introText = new Text("", 2, 0);
|
|
6
|
+
|
|
7
|
+
constructor(private readonly onNext: () => void) {}
|
|
8
|
+
|
|
9
|
+
invalidate() {
|
|
10
|
+
this.introText.invalidate();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
render(width: number): string[] {
|
|
14
|
+
this.introText.setText(
|
|
15
|
+
"Guardrails helps prevent accidental exposure of secrets and risky actions.\n\nIt gives you two protections:\n- Policies: file access rules (`noAccess` or `readOnly`)\n- Permission gate: confirmation before dangerous commands run\n\nYou are choosing the starting defaults now. You can change them later in `/guardrails:settings`.",
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
return [
|
|
19
|
+
" Welcome to Guardrails",
|
|
20
|
+
"",
|
|
21
|
+
...this.introText.render(Math.max(1, width)),
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
handleInput(data: string): void {
|
|
26
|
+
if (matchesKey(data, Key.enter)) {
|
|
27
|
+
this.onNext();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Wizard } from "@aliou/pi-utils-settings";
|
|
2
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
4
|
+
import { Key, matchesKey } from "@earendil-works/pi-tui";
|
|
5
|
+
import {
|
|
6
|
+
OnboardingDefaultsChoiceStep,
|
|
7
|
+
OnboardingPathAccessStep,
|
|
8
|
+
} from "./onboarding-choice-step";
|
|
9
|
+
import { OnboardingFinishStep } from "./onboarding-finish-step";
|
|
10
|
+
import { OnboardingIntroStep } from "./onboarding-intro-step";
|
|
11
|
+
import type { OnboardingResult, OnboardingState } from "./onboarding-types";
|
|
12
|
+
|
|
13
|
+
export type { OnboardingResult } from "./onboarding-types";
|
|
14
|
+
|
|
15
|
+
export function createOnboardingWizard(
|
|
16
|
+
theme: Theme,
|
|
17
|
+
done: (result: OnboardingResult) => void,
|
|
18
|
+
): Component {
|
|
19
|
+
const state: OnboardingState = {
|
|
20
|
+
applyBuiltinDefaults: null,
|
|
21
|
+
pathAccessEnabled: null,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
let markWelcomeComplete: (() => void) | null = null;
|
|
25
|
+
let settled = false;
|
|
26
|
+
|
|
27
|
+
const finalize = (result: OnboardingResult) => {
|
|
28
|
+
if (settled) return;
|
|
29
|
+
settled = true;
|
|
30
|
+
done(result);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const wizard = new Wizard({
|
|
34
|
+
title: "Guardrails onboarding",
|
|
35
|
+
theme,
|
|
36
|
+
steps: [
|
|
37
|
+
{
|
|
38
|
+
label: "Welcome",
|
|
39
|
+
build: (ctx) => {
|
|
40
|
+
markWelcomeComplete = ctx.markComplete;
|
|
41
|
+
return new OnboardingIntroStep(() => {
|
|
42
|
+
ctx.markComplete();
|
|
43
|
+
ctx.goNext();
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
label: "Defaults",
|
|
49
|
+
build: (ctx) =>
|
|
50
|
+
new OnboardingDefaultsChoiceStep(state, theme, () => {
|
|
51
|
+
ctx.markComplete();
|
|
52
|
+
ctx.goNext();
|
|
53
|
+
}),
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
label: "Path access",
|
|
57
|
+
build: (ctx) =>
|
|
58
|
+
new OnboardingPathAccessStep(state, theme, () => {
|
|
59
|
+
ctx.markComplete();
|
|
60
|
+
ctx.goNext();
|
|
61
|
+
}),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
label: "Recap",
|
|
65
|
+
build: (ctx) =>
|
|
66
|
+
new OnboardingFinishStep(state, () => {
|
|
67
|
+
if (state.applyBuiltinDefaults === null) return;
|
|
68
|
+
ctx.markComplete();
|
|
69
|
+
finalize({
|
|
70
|
+
completed: true,
|
|
71
|
+
applyBuiltinDefaults: state.applyBuiltinDefaults,
|
|
72
|
+
pathAccessEnabled: state.pathAccessEnabled,
|
|
73
|
+
});
|
|
74
|
+
}),
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
onComplete: () => {
|
|
78
|
+
if (state.applyBuiltinDefaults === null) {
|
|
79
|
+
finalize({
|
|
80
|
+
completed: false,
|
|
81
|
+
applyBuiltinDefaults: null,
|
|
82
|
+
pathAccessEnabled: null,
|
|
83
|
+
});
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
finalize({
|
|
87
|
+
completed: true,
|
|
88
|
+
applyBuiltinDefaults: state.applyBuiltinDefaults,
|
|
89
|
+
pathAccessEnabled: state.pathAccessEnabled,
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
onCancel: () =>
|
|
93
|
+
finalize({
|
|
94
|
+
completed: false,
|
|
95
|
+
applyBuiltinDefaults: null,
|
|
96
|
+
pathAccessEnabled: null,
|
|
97
|
+
}),
|
|
98
|
+
hintSuffix: "Enter select/continue",
|
|
99
|
+
minContentHeight: 12,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
render: (width) => wizard.render(width),
|
|
104
|
+
invalidate: () => wizard.invalidate(),
|
|
105
|
+
handleInput: (data: string) => {
|
|
106
|
+
if (
|
|
107
|
+
matchesKey(data, Key.tab) &&
|
|
108
|
+
wizard.getActiveIndex() === 0 &&
|
|
109
|
+
markWelcomeComplete
|
|
110
|
+
) {
|
|
111
|
+
markWelcomeComplete();
|
|
112
|
+
}
|
|
113
|
+
wizard.handleInput(data);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|