pi-permission-system 0.2.2 → 0.3.0
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 +19 -0
- package/README.md +1 -1
- package/config.json +2 -1
- package/package.json +5 -4
- package/src/config-modal-test.ts +217 -0
- package/src/config-modal.ts +231 -0
- package/src/extension-config.ts +45 -3
- package/src/index.ts +183 -135
- package/src/permission-dialog.ts +83 -0
- package/src/permission-forwarding.ts +102 -0
- package/src/test.ts +210 -2
- package/src/types-shims.d.ts +166 -0
- package/src/yolo-mode.ts +23 -0
- package/src/zellij-modal.ts +1001 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.0] - 2026-03-23
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Yolo mode for auto-approval when enabled — bypasses permission prompts for streamlined workflows
|
|
12
|
+
- Permission forwarding system for subagent-to-primary IPC communication
|
|
13
|
+
- Configuration modal UI with Zellij integration (`config-modal.ts`, `zellij-modal.ts`)
|
|
14
|
+
- `permission-forwarding.ts` module for subagent permission request routing
|
|
15
|
+
- `yolo-mode.ts` module for automatic permission approval when yolo mode is active
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Updated `@mariozechner/pi-coding-agent` and `@mariozechner/pi-tui` peer dependencies to ^0.62.0
|
|
19
|
+
- Refactored `index.ts` to export new permission resolution utilities
|
|
20
|
+
- Expanded `extension-config.ts` with config normalization for new features
|
|
21
|
+
- Added `types-shims.d.ts` for Zellij modal type definitions
|
|
22
|
+
|
|
23
|
+
### Tests
|
|
24
|
+
- Added comprehensive tests for config modal functionality
|
|
25
|
+
- Added tests for permission forwarding behavior
|
|
26
|
+
|
|
8
27
|
## [0.2.2] - 2026-03-13
|
|
9
28
|
|
|
10
29
|
### Changed
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 🔐 pi-permission-system
|
|
2
2
|
|
|
3
|
-
[](package.json)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
|
|
6
6
|
Permission enforcement extension for the Pi coding agent that provides centralized, deterministic permission gates for tool, bash, MCP, skill, and special operations.
|
package/config.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-permission-system",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Permission enforcement extension for the Pi coding agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"scripts": {
|
|
22
22
|
"build": "npx --yes -p typescript@5.7.3 tsc -p tsconfig.json --noCheck",
|
|
23
23
|
"lint": "npm run build",
|
|
24
|
-
"test": "bun ./src/test.ts",
|
|
24
|
+
"test": "bun ./src/test.ts && bun ./src/config-modal-test.ts",
|
|
25
25
|
"check": "npm run lint && npm run test"
|
|
26
26
|
},
|
|
27
27
|
"keywords": [
|
|
@@ -54,7 +54,8 @@
|
|
|
54
54
|
]
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"@mariozechner/pi-coding-agent": "
|
|
58
|
-
"@
|
|
57
|
+
"@mariozechner/pi-coding-agent": "^0.62.0",
|
|
58
|
+
"@mariozechner/pi-tui": "^0.62.0",
|
|
59
|
+
"@sinclair/typebox": "^0.34.48"
|
|
59
60
|
}
|
|
60
61
|
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { mock } from "bun:test";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_EXTENSION_CONFIG,
|
|
9
|
+
loadPermissionSystemConfig,
|
|
10
|
+
savePermissionSystemConfig,
|
|
11
|
+
type PermissionSystemExtensionConfig,
|
|
12
|
+
} from "./extension-config.js";
|
|
13
|
+
|
|
14
|
+
mock.module("@mariozechner/pi-coding-agent", () => ({
|
|
15
|
+
getSettingsListTheme: () => ({}),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
mock.module("@mariozechner/pi-tui", () => ({
|
|
19
|
+
Box: class {},
|
|
20
|
+
Container: class {
|
|
21
|
+
addChild(): void {}
|
|
22
|
+
render(): string[] {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
invalidate(): void {}
|
|
26
|
+
},
|
|
27
|
+
SettingsList: class {
|
|
28
|
+
handleInput(): void {}
|
|
29
|
+
updateValue(): void {}
|
|
30
|
+
render(): string[] {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
invalidate(): void {}
|
|
34
|
+
},
|
|
35
|
+
Spacer: class {},
|
|
36
|
+
Text: class {},
|
|
37
|
+
truncateToWidth: (text: string) => text,
|
|
38
|
+
visibleWidth: (text: string) => text.length,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
const { registerPermissionSystemCommand } = await import("./config-modal.js");
|
|
42
|
+
|
|
43
|
+
type Notification = { message: string; level: "info" | "warning" | "error" };
|
|
44
|
+
|
|
45
|
+
type CommandContextStub = {
|
|
46
|
+
hasUI: boolean;
|
|
47
|
+
ui: {
|
|
48
|
+
notify(message: string, level: "info" | "warning" | "error"): void;
|
|
49
|
+
custom<T>(renderer: (...args: unknown[]) => unknown, options?: unknown): Promise<T>;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function runTest(name: string, testFn: () => void): void {
|
|
54
|
+
testFn();
|
|
55
|
+
console.log(`[PASS] ${name}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function runAsyncTest(name: string, testFn: () => Promise<void>): Promise<void> {
|
|
59
|
+
await testFn();
|
|
60
|
+
console.log(`[PASS] ${name}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function createCommandContext(
|
|
64
|
+
hasUI: boolean,
|
|
65
|
+
): { ctx: CommandContextStub; notifications: Notification[]; getCustomCalls(): number } {
|
|
66
|
+
const notifications: Notification[] = [];
|
|
67
|
+
let customCalls = 0;
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
ctx: {
|
|
71
|
+
hasUI,
|
|
72
|
+
ui: {
|
|
73
|
+
notify(message: string, level: "info" | "warning" | "error") {
|
|
74
|
+
notifications.push({ message, level });
|
|
75
|
+
},
|
|
76
|
+
async custom<T>(_renderer: (...args: unknown[]) => unknown, _options?: unknown): Promise<T> {
|
|
77
|
+
customCalls += 1;
|
|
78
|
+
return undefined as T;
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
notifications,
|
|
83
|
+
getCustomCalls: () => customCalls,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function lastNotification(notifications: Notification[]): Notification {
|
|
88
|
+
return notifications[notifications.length - 1] as Notification;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
runTest("permission-system command completions expose top-level config actions", () => {
|
|
92
|
+
const baseDir = mkdtempSync(join(tmpdir(), "pi-permission-system-command-completions-"));
|
|
93
|
+
const configPath = join(baseDir, "config.json");
|
|
94
|
+
let config: PermissionSystemExtensionConfig = { ...DEFAULT_EXTENSION_CONFIG };
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const controller = {
|
|
98
|
+
getConfig: () => config,
|
|
99
|
+
setConfig: (next: PermissionSystemExtensionConfig) => {
|
|
100
|
+
config = next;
|
|
101
|
+
},
|
|
102
|
+
getConfigPath: () => configPath,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
let definition: {
|
|
106
|
+
description: string;
|
|
107
|
+
getArgumentCompletions?: (argumentPrefix: string) => Array<{ value: string; label: string; description?: string }> | null;
|
|
108
|
+
handler: (args: string, ctx: CommandContextStub) => Promise<void>;
|
|
109
|
+
} | null = null;
|
|
110
|
+
|
|
111
|
+
registerPermissionSystemCommand(
|
|
112
|
+
{
|
|
113
|
+
registerCommand(_name: string, nextDefinition: typeof definition) {
|
|
114
|
+
definition = nextDefinition;
|
|
115
|
+
},
|
|
116
|
+
} as never,
|
|
117
|
+
controller as never,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
assert.ok(definition !== null);
|
|
121
|
+
assert.ok(typeof definition?.getArgumentCompletions === "function");
|
|
122
|
+
|
|
123
|
+
const topLevel = definition?.getArgumentCompletions?.("");
|
|
124
|
+
assert.ok(Array.isArray(topLevel));
|
|
125
|
+
assert.ok(topLevel?.some((item) => item.value === "show"));
|
|
126
|
+
assert.ok(topLevel?.some((item) => item.value === "reset"));
|
|
127
|
+
|
|
128
|
+
const filtered = definition?.getArgumentCompletions?.("pa");
|
|
129
|
+
assert.deepEqual(filtered?.map((item) => item.value), ["path"]);
|
|
130
|
+
assert.equal(definition?.getArgumentCompletions?.("path extra"), null);
|
|
131
|
+
assert.equal(definition?.getArgumentCompletions?.("zzz"), null);
|
|
132
|
+
} finally {
|
|
133
|
+
rmSync(baseDir, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await runAsyncTest("permission-system command handlers manage config summary, persistence, and modal routing", async () => {
|
|
138
|
+
const baseDir = mkdtempSync(join(tmpdir(), "pi-permission-system-command-"));
|
|
139
|
+
const configPath = join(baseDir, "config.json");
|
|
140
|
+
let config: PermissionSystemExtensionConfig = {
|
|
141
|
+
debugLog: true,
|
|
142
|
+
permissionReviewLog: false,
|
|
143
|
+
yoloMode: true,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const initialSave = savePermissionSystemConfig(config, configPath);
|
|
148
|
+
assert.equal(initialSave.success, true);
|
|
149
|
+
|
|
150
|
+
const controller = {
|
|
151
|
+
getConfig: () => config,
|
|
152
|
+
setConfig: (next: PermissionSystemExtensionConfig) => {
|
|
153
|
+
const normalized = loadPermissionSystemConfig(configPath).config;
|
|
154
|
+
const saved = savePermissionSystemConfig(next, configPath);
|
|
155
|
+
assert.equal(saved.success, true);
|
|
156
|
+
config = loadPermissionSystemConfig(configPath).config;
|
|
157
|
+
assert.notDeepEqual(config, normalized);
|
|
158
|
+
},
|
|
159
|
+
getConfigPath: () => configPath,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
let registeredName = "";
|
|
163
|
+
let definition: {
|
|
164
|
+
description: string;
|
|
165
|
+
getArgumentCompletions?: (argumentPrefix: string) => Array<{ value: string; label: string; description?: string }> | null;
|
|
166
|
+
handler: (args: string, ctx: CommandContextStub) => Promise<void>;
|
|
167
|
+
} | null = null;
|
|
168
|
+
|
|
169
|
+
registerPermissionSystemCommand(
|
|
170
|
+
{
|
|
171
|
+
registerCommand(name: string, nextDefinition: typeof definition) {
|
|
172
|
+
registeredName = name;
|
|
173
|
+
definition = nextDefinition;
|
|
174
|
+
},
|
|
175
|
+
} as never,
|
|
176
|
+
controller as never,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
assert.equal(registeredName, "permission-system");
|
|
180
|
+
assert.ok(definition !== null);
|
|
181
|
+
assert.ok((definition?.description ?? "").includes("Configure pi-permission-system"));
|
|
182
|
+
|
|
183
|
+
const infoCtx = createCommandContext(true);
|
|
184
|
+
await definition?.handler("show", infoCtx.ctx);
|
|
185
|
+
assert.ok(lastNotification(infoCtx.notifications).message.includes("yoloMode=on"));
|
|
186
|
+
assert.ok(lastNotification(infoCtx.notifications).message.includes("debugLog=on"));
|
|
187
|
+
|
|
188
|
+
await definition?.handler("path", infoCtx.ctx);
|
|
189
|
+
assert.equal(lastNotification(infoCtx.notifications).message, `permission-system config: ${configPath}`);
|
|
190
|
+
|
|
191
|
+
await definition?.handler("help", infoCtx.ctx);
|
|
192
|
+
assert.ok(lastNotification(infoCtx.notifications).message.includes("Usage: /permission-system"));
|
|
193
|
+
|
|
194
|
+
await definition?.handler("reset", infoCtx.ctx);
|
|
195
|
+
assert.deepEqual(config, DEFAULT_EXTENSION_CONFIG);
|
|
196
|
+
assert.equal(lastNotification(infoCtx.notifications).message, "Permission system settings reset to defaults.");
|
|
197
|
+
|
|
198
|
+
const persisted = JSON.parse(readFileSync(configPath, "utf8")) as Record<string, unknown>;
|
|
199
|
+
assert.deepEqual(persisted, DEFAULT_EXTENSION_CONFIG);
|
|
200
|
+
|
|
201
|
+
await definition?.handler("unknown", infoCtx.ctx);
|
|
202
|
+
assert.equal(lastNotification(infoCtx.notifications).level, "warning");
|
|
203
|
+
assert.ok(lastNotification(infoCtx.notifications).message.includes("Usage: /permission-system"));
|
|
204
|
+
|
|
205
|
+
const headlessCtx = createCommandContext(false);
|
|
206
|
+
await definition?.handler("", headlessCtx.ctx);
|
|
207
|
+
assert.equal(lastNotification(headlessCtx.notifications).message, "/permission-system requires interactive TUI mode.");
|
|
208
|
+
|
|
209
|
+
const modalCtx = createCommandContext(true);
|
|
210
|
+
await definition?.handler("", modalCtx.ctx);
|
|
211
|
+
assert.equal(modalCtx.getCustomCalls(), 1);
|
|
212
|
+
} finally {
|
|
213
|
+
rmSync(baseDir, { recursive: true, force: true });
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
console.log("All permission-system config-modal tests passed.");
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { SettingItem } from "@mariozechner/pi-tui";
|
|
3
|
+
|
|
4
|
+
import { DEFAULT_EXTENSION_CONFIG, type PermissionSystemExtensionConfig } from "./extension-config.js";
|
|
5
|
+
import { ZellijModal, ZellijSettingsModal } from "./zellij-modal.js";
|
|
6
|
+
|
|
7
|
+
interface PermissionSystemConfigController {
|
|
8
|
+
getConfig(): PermissionSystemExtensionConfig;
|
|
9
|
+
setConfig(next: PermissionSystemExtensionConfig, ctx: ExtensionCommandContext): void;
|
|
10
|
+
getConfigPath(): string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface SettingValueSyncTarget {
|
|
14
|
+
updateValue(id: string, value: string): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const ON_OFF = ["on", "off"];
|
|
18
|
+
const COMMAND_ARGUMENTS = [
|
|
19
|
+
{
|
|
20
|
+
value: "show",
|
|
21
|
+
label: "Show active settings",
|
|
22
|
+
description: "Display the current permission-system config summary",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
value: "path",
|
|
26
|
+
label: "Show config path",
|
|
27
|
+
description: "Display the config.json path used by pi-permission-system",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
value: "reset",
|
|
31
|
+
label: "Reset defaults",
|
|
32
|
+
description: "Restore default yolo/logging settings and persist them",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
value: "help",
|
|
36
|
+
label: "Show help",
|
|
37
|
+
description: "Display command usage",
|
|
38
|
+
},
|
|
39
|
+
] as const;
|
|
40
|
+
const USAGE_TEXT = "Usage: /permission-system [show|path|reset|help] (or run /permission-system with no args to open settings modal)";
|
|
41
|
+
|
|
42
|
+
function cloneDefaultConfig(): PermissionSystemExtensionConfig {
|
|
43
|
+
return {
|
|
44
|
+
debugLog: DEFAULT_EXTENSION_CONFIG.debugLog,
|
|
45
|
+
permissionReviewLog: DEFAULT_EXTENSION_CONFIG.permissionReviewLog,
|
|
46
|
+
yoloMode: DEFAULT_EXTENSION_CONFIG.yoloMode,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function toOnOff(value: boolean): string {
|
|
51
|
+
return value ? "on" : "off";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function summarizeConfig(config: PermissionSystemExtensionConfig): string {
|
|
55
|
+
return [
|
|
56
|
+
`yoloMode=${toOnOff(config.yoloMode)}`,
|
|
57
|
+
`permissionReviewLog=${toOnOff(config.permissionReviewLog)}`,
|
|
58
|
+
`debugLog=${toOnOff(config.debugLog)}`,
|
|
59
|
+
].join(", ");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildSettingItems(config: PermissionSystemExtensionConfig): SettingItem[] {
|
|
63
|
+
return [
|
|
64
|
+
{
|
|
65
|
+
id: "yoloMode",
|
|
66
|
+
label: "YOLO mode",
|
|
67
|
+
description: "Auto-approve ask-state permission checks, including subagent approval forwarding",
|
|
68
|
+
currentValue: toOnOff(config.yoloMode),
|
|
69
|
+
values: ON_OFF,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "permissionReviewLog",
|
|
73
|
+
label: "Permission review log",
|
|
74
|
+
description: "Write permission request and decision audit events to the extension logs directory",
|
|
75
|
+
currentValue: toOnOff(config.permissionReviewLog),
|
|
76
|
+
values: ON_OFF,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: "debugLog",
|
|
80
|
+
label: "Debug logging",
|
|
81
|
+
description: "Write verbose permission-system diagnostics to the extension logs directory",
|
|
82
|
+
currentValue: toOnOff(config.debugLog),
|
|
83
|
+
values: ON_OFF,
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function applySetting(
|
|
89
|
+
config: PermissionSystemExtensionConfig,
|
|
90
|
+
id: string,
|
|
91
|
+
value: string,
|
|
92
|
+
): PermissionSystemExtensionConfig {
|
|
93
|
+
switch (id) {
|
|
94
|
+
case "yoloMode":
|
|
95
|
+
return { ...config, yoloMode: value === "on" };
|
|
96
|
+
case "permissionReviewLog":
|
|
97
|
+
return { ...config, permissionReviewLog: value === "on" };
|
|
98
|
+
case "debugLog":
|
|
99
|
+
return { ...config, debugLog: value === "on" };
|
|
100
|
+
default:
|
|
101
|
+
return config;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function syncSettingValues(settingsList: SettingValueSyncTarget, config: PermissionSystemExtensionConfig): void {
|
|
106
|
+
settingsList.updateValue("yoloMode", toOnOff(config.yoloMode));
|
|
107
|
+
settingsList.updateValue("permissionReviewLog", toOnOff(config.permissionReviewLog));
|
|
108
|
+
settingsList.updateValue("debugLog", toOnOff(config.debugLog));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getArgumentCompletions(argumentPrefix: string): Array<{ value: string; label: string; description: string }> | null {
|
|
112
|
+
const normalized = argumentPrefix.trim().toLowerCase();
|
|
113
|
+
if (normalized.includes(" ")) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const filtered = COMMAND_ARGUMENTS.filter((item) => item.value.startsWith(normalized));
|
|
118
|
+
return filtered.length > 0 ? [...filtered] : null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function openSettingsModal(ctx: ExtensionCommandContext, controller: PermissionSystemConfigController): Promise<void> {
|
|
122
|
+
const overlayOptions = { anchor: "center" as const, width: 82, maxHeight: "85%" as const, margin: 1 };
|
|
123
|
+
|
|
124
|
+
await ctx.ui.custom<void>(
|
|
125
|
+
(tui, theme, _keybindings, done) => {
|
|
126
|
+
let current = controller.getConfig();
|
|
127
|
+
let settingsModal: ZellijSettingsModal | null = null;
|
|
128
|
+
|
|
129
|
+
settingsModal = new ZellijSettingsModal(
|
|
130
|
+
{
|
|
131
|
+
title: "Permission System Settings",
|
|
132
|
+
description: "Local extension options for permission logging and auto-approval behavior",
|
|
133
|
+
settings: buildSettingItems(current),
|
|
134
|
+
onChange: (id, newValue) => {
|
|
135
|
+
current = applySetting(current, id, newValue);
|
|
136
|
+
controller.setConfig(current, ctx);
|
|
137
|
+
current = controller.getConfig();
|
|
138
|
+
if (settingsModal) {
|
|
139
|
+
syncSettingValues(settingsModal, current);
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
onClose: () => done(),
|
|
143
|
+
helpText: `/permission-system show • /permission-system reset • ${controller.getConfigPath()}`,
|
|
144
|
+
enableSearch: true,
|
|
145
|
+
},
|
|
146
|
+
theme,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const modal = new ZellijModal(
|
|
150
|
+
settingsModal,
|
|
151
|
+
{
|
|
152
|
+
borderStyle: "rounded",
|
|
153
|
+
titleBar: {
|
|
154
|
+
left: "Permission System Settings",
|
|
155
|
+
right: "pi-permission-system",
|
|
156
|
+
},
|
|
157
|
+
helpUndertitle: {
|
|
158
|
+
text: "Esc: close | ↑↓: navigate | Space: toggle",
|
|
159
|
+
color: "dim",
|
|
160
|
+
},
|
|
161
|
+
overlay: overlayOptions,
|
|
162
|
+
},
|
|
163
|
+
theme,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
render(width: number) {
|
|
168
|
+
return modal.renderModal(width).lines;
|
|
169
|
+
},
|
|
170
|
+
invalidate() {
|
|
171
|
+
modal.invalidate();
|
|
172
|
+
},
|
|
173
|
+
handleInput(data: string) {
|
|
174
|
+
modal.handleInput(data);
|
|
175
|
+
tui.requestRender();
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
{ overlay: true, overlayOptions },
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function handleArgs(args: string, ctx: ExtensionCommandContext, controller: PermissionSystemConfigController): boolean {
|
|
184
|
+
const normalized = args.trim().toLowerCase();
|
|
185
|
+
if (!normalized) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (normalized === "show") {
|
|
190
|
+
ctx.ui.notify(`permission-system: ${summarizeConfig(controller.getConfig())}`, "info");
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (normalized === "path") {
|
|
195
|
+
ctx.ui.notify(`permission-system config: ${controller.getConfigPath()}`, "info");
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (normalized === "reset") {
|
|
200
|
+
controller.setConfig(cloneDefaultConfig(), ctx);
|
|
201
|
+
ctx.ui.notify("Permission system settings reset to defaults.", "info");
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (normalized === "help") {
|
|
206
|
+
ctx.ui.notify(USAGE_TEXT, "info");
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
ctx.ui.notify(USAGE_TEXT, "warning");
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function registerPermissionSystemCommand(pi: ExtensionAPI, controller: PermissionSystemConfigController): void {
|
|
215
|
+
pi.registerCommand("permission-system", {
|
|
216
|
+
description: "Configure pi-permission-system logging and yolo-mode behavior",
|
|
217
|
+
getArgumentCompletions,
|
|
218
|
+
handler: async (args, ctx) => {
|
|
219
|
+
if (handleArgs(args, ctx, controller)) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!ctx.hasUI) {
|
|
224
|
+
ctx.ui.notify("/permission-system requires interactive TUI mode.", "warning");
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
await openSettingsModal(ctx, controller);
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
}
|
package/src/extension-config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
|
|
@@ -9,6 +9,7 @@ export const EXTENSION_ID = "pi-permission-system";
|
|
|
9
9
|
export interface PermissionSystemExtensionConfig {
|
|
10
10
|
debugLog: boolean;
|
|
11
11
|
permissionReviewLog: boolean;
|
|
12
|
+
yoloMode: boolean;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export interface PermissionSystemConfigLoadResult {
|
|
@@ -17,9 +18,15 @@ export interface PermissionSystemConfigLoadResult {
|
|
|
17
18
|
warning?: string;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
export interface PermissionSystemConfigSaveResult {
|
|
22
|
+
success: boolean;
|
|
23
|
+
error?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
20
26
|
export const DEFAULT_EXTENSION_CONFIG: PermissionSystemExtensionConfig = {
|
|
21
27
|
debugLog: false,
|
|
22
28
|
permissionReviewLog: true,
|
|
29
|
+
yoloMode: false,
|
|
23
30
|
};
|
|
24
31
|
|
|
25
32
|
export function resolveExtensionRoot(moduleUrl = import.meta.url): string {
|
|
@@ -36,6 +43,7 @@ function cloneDefaultConfig(): PermissionSystemExtensionConfig {
|
|
|
36
43
|
return {
|
|
37
44
|
debugLog: DEFAULT_EXTENSION_CONFIG.debugLog,
|
|
38
45
|
permissionReviewLog: DEFAULT_EXTENSION_CONFIG.permissionReviewLog,
|
|
46
|
+
yoloMode: DEFAULT_EXTENSION_CONFIG.yoloMode,
|
|
39
47
|
};
|
|
40
48
|
}
|
|
41
49
|
|
|
@@ -43,11 +51,12 @@ function createDefaultConfigContent(): string {
|
|
|
43
51
|
return `${JSON.stringify(DEFAULT_EXTENSION_CONFIG, null, 2)}\n`;
|
|
44
52
|
}
|
|
45
53
|
|
|
46
|
-
function
|
|
54
|
+
export function normalizePermissionSystemConfig(raw: unknown): PermissionSystemExtensionConfig {
|
|
47
55
|
const record = toRecord(raw);
|
|
48
56
|
return {
|
|
49
57
|
debugLog: record.debugLog === true,
|
|
50
58
|
permissionReviewLog: record.permissionReviewLog !== false,
|
|
59
|
+
yoloMode: record.yoloMode === true,
|
|
51
60
|
};
|
|
52
61
|
}
|
|
53
62
|
|
|
@@ -79,7 +88,7 @@ export function loadPermissionSystemConfig(configPath = CONFIG_PATH): Permission
|
|
|
79
88
|
try {
|
|
80
89
|
const raw = readFileSync(configPath, "utf-8");
|
|
81
90
|
const parsed = JSON.parse(raw) as unknown;
|
|
82
|
-
const config =
|
|
91
|
+
const config = normalizePermissionSystemConfig(parsed);
|
|
83
92
|
return {
|
|
84
93
|
config,
|
|
85
94
|
created: ensureResult.created,
|
|
@@ -95,6 +104,39 @@ export function loadPermissionSystemConfig(configPath = CONFIG_PATH): Permission
|
|
|
95
104
|
}
|
|
96
105
|
}
|
|
97
106
|
|
|
107
|
+
export function savePermissionSystemConfig(
|
|
108
|
+
config: PermissionSystemExtensionConfig,
|
|
109
|
+
configPath = CONFIG_PATH,
|
|
110
|
+
): PermissionSystemConfigSaveResult {
|
|
111
|
+
const normalized = normalizePermissionSystemConfig(config);
|
|
112
|
+
const tmpPath = `${configPath}.tmp`;
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
ensureConfigDirectory(configPath);
|
|
116
|
+
writeFileSync(tmpPath, `${JSON.stringify(normalized, null, 2)}\n`, "utf-8");
|
|
117
|
+
renameSync(tmpPath, configPath);
|
|
118
|
+
return { success: true };
|
|
119
|
+
} catch (error) {
|
|
120
|
+
try {
|
|
121
|
+
if (existsSync(tmpPath)) {
|
|
122
|
+
unlinkSync(tmpPath);
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
// Ignore cleanup failures.
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
129
|
+
return {
|
|
130
|
+
success: false,
|
|
131
|
+
error: `Failed to save permission-system config at '${configPath}': ${message}`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function getPermissionSystemConfigPath(configPath = CONFIG_PATH): string {
|
|
137
|
+
return configPath;
|
|
138
|
+
}
|
|
139
|
+
|
|
98
140
|
export function ensurePermissionSystemLogsDirectory(logsDir = LOGS_DIR): string | undefined {
|
|
99
141
|
try {
|
|
100
142
|
mkdirSync(logsDir, { recursive: true });
|