Edit plugins id & docs

Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
Hadi
2026-05-20 15:09:09 +02:00
parent 0e982c6ade
commit 2c63cdbeff
5 changed files with 86 additions and 96 deletions
+17 -49
View File
@@ -34,13 +34,13 @@ Plugin = {
### Hook reference ### Hook reference
| Hook | When called | Sync/async | Return value | | Hook | When called | Sync/async | Return value |
| ------------------------- | ------------------------------------- | ------------ | ----------------------------------------------- | | ------------------------- | ------------------------------------- | ------------ | ----------------------------------------------------------------------------------------- |
| `on_config()` | At startup and on config save | always sync | ignored | | `on_config()` | At startup and on config save | always sync | ignored |
| `on_start()` | Once at startup, after `on_config` | configurable | `false` to self-disable the plugin, otherwise ignored | | `on_start()` | Once at startup, after `on_config` | configurable | `false` to self-disable the plugin, otherwise ignored |
| `on_quit()` | When the app exits | always sync | ignored | | `on_quit()` | When the app exits | always sync | ignored |
| `on_request(req)` | Every request, before auto-forward | configurable | `"drop"`, `"forward"`, or `nil` (sync only) | | `on_request(req)` | Every request, before auto-forward | configurable | `"drop"`, `"forward"`, or `nil` (nil does nothing and le the user/TUI choose) (sync only) |
| `on_response(req, res)` | Every response | configurable | `"drop"`, `"forward"`, or `nil` (sync only) | | `on_response(req, res)` | Every response | configurable | `"drop"`, `"forward"`, or `nil` (nil does nothing and le the user/TUI choose) (sync only) |
| `on_history_entry(entry)` | Sync: before DB insert / Async: after | configurable | `"skip"` (don't save), `"keep"` or `nil` (save) -- sync only | | `on_history_entry(entry)` | Sync: before DB insert / Async: after | configurable | `"skip"` (don't save), `"keep"` or `nil` (save) (sync only) |
## Request and response objects ## Request and response objects
@@ -130,17 +130,19 @@ local cfg = get_config()
### Finding deduplication ### Finding deduplication
A finding is identified by `(plugin_name, key)`. If a finding with that pair already exists in the database it will **not** be re-created, even across restarts. If the user **dismisses** a finding it is permanently hidden and will never reappear, even if the plugin generates it again. A finding is identified by `(plugin_name, key)`. If a finding with that pair already exists in the database it will **not** be re-created, even across restarts.
If the user **dismisses** a finding it is permanently hidden and will never reappear, even if the plugin generates it again.
## Configuration ## Configuration
Plugin configuration is stored in a `plugins.yaml` file alongside the project database. Each plugin has an `enable` toggle and an optional `config` block (arbitrary YAML). Plugin configuration is stored in a `plugins.yaml` file alongside the project database.
Each plugin is keyed by its filename (without the `.lua` extension) and has an `enable` toggle and an optional `config` block (arbitrary YAML).
```yaml ```yaml
plugins: plugins:
my_plugin: my_plugin:
enable: true enable: true
config: | config:
some_key: some_value some_key: some_value
list: list:
- item1 - item1
@@ -149,9 +151,11 @@ plugins:
enable: false enable: false
``` ```
The config block is edited from the **Plugins** page in the TUI. Inside a plugin, call `get_config()` to retrieve the config as a Lua table. The config block is edited from the **Plugins** page in the TUI.
Inside a plugin, call `get_config()` to retrieve the config as a Lua table.
`on_config()` is called once at startup (before `on_start`) and again every time the user saves the config in the TUI. It is the right place to read `get_config()` and populate local variables. `on_config()` is called once at startup (before `on_start`) and again every time the user saves the config in the TUI.
It is the right place to read `get_config()` and populate local variables.
```lua ```lua
local items = {} local items = {}
@@ -174,49 +178,13 @@ end
`on_config` and `on_quit` are always synchronous regardless of the Plugin table declaration. `on_config` and `on_quit` are always synchronous regardless of the Plugin table declaration.
### Return values for sync hooks Sync `on_history_entry` runs **before** the DB insert, so it can prevent an entry from ever appearing in history.
Async `on_history_entry` runs **after** the insert and cannot affect it.
**`on_start`:**
| Return value | Effect |
| ------------ | -------------------------------------------------------------------------------------------- |
| `false` | The plugin is disabled immediately and the state is persisted (equivalent to toggling it off). |
| anything else | Ignored. |
This is useful for prerequisite checks (binary not found, config invalid, etc.) so the plugin does not silently run in a broken state:
```lua
function on_start()
local h = io.popen("command -v mytool 2>/dev/null")
local result = h and h:read("*a") or ""
if h then h:close() end
if result:match("^%s*$") then
notif("MyPlugin", "mytool not found, plugin disabled", "error")
return false
end
end
```
**`on_request` and `on_response`:**
| Return value | Effect |
| ------------ | --------------------------------------------------------------------------------- |
| `"drop"` | The flow is dropped immediately and never shown in the intercept panel. |
| `"forward"` | The flow is forwarded immediately without going through the intercept panel. |
| `nil` | Normal behaviour: the flow appears in the intercept panel for the user to decide. |
**`on_history_entry` (sync only):**
| Return value | Effect |
| ----------------- | --------------------------------- |
| `"skip"` | The entry is not saved to the DB. |
| `"keep"` or `nil` | The entry is saved normally. |
Sync `on_history_entry` runs **before** the DB insert, so it can prevent an entry from ever appearing in history. Async `on_history_entry` runs **after** the insert and cannot affect it.
## Priority ## Priority
Plugins with a higher `priority` value run before plugins with a lower value (default `0`). This matters for sync hooks that return a decision: the first plugin to return a non-nil value short-circuits the remaining plugins. Plugins with a higher `priority` value run before plugins with a lower value (default `0`).
This matters for sync hooks that return a decision: the first plugin to return a non-nil value short-circuits the remaining plugins.
```lua ```lua
Plugin = { Plugin = {
+17 -16
View File
@@ -68,11 +68,11 @@ func (m *Manager) LoadFromDir(dir string) error {
continue continue
} }
if m.pluginsFile != nil { if m.pluginsFile != nil {
if enabled, configText, found := m.pluginsFile.get(p.Name); found { if enabled, configText, found := m.pluginsFile.get(p.ID); found {
p.Enabled = enabled p.Enabled = enabled
p.ConfigText = configText p.ConfigText = configText
} else { } else {
m.pluginsFile.register(p.Name, p.Enabled) m.pluginsFile.register(p.ID, p.Enabled)
} }
} }
m.mu.Lock() m.mu.Lock()
@@ -87,6 +87,7 @@ func (m *Manager) LoadFromDir(dir string) error {
func (m *Manager) loadPlugin(path string) (*Plugin, error) { func (m *Manager) loadPlugin(path string) (*Plugin, error) {
p := &Plugin{ p := &Plugin{
ID: strings.TrimSuffix(filepath.Base(path), ".lua"),
FilePath: path, FilePath: path,
Enabled: true, Enabled: true,
hooks: make(map[string]HookConfig), hooks: make(map[string]HookConfig),
@@ -107,7 +108,7 @@ func (m *Manager) loadPlugin(path string) (*Plugin, error) {
p.Name = string(s) p.Name = string(s)
} }
if p.Name == "" { if p.Name == "" {
p.Name = strings.TrimSuffix(filepath.Base(path), ".lua") p.Name = p.ID
} }
if s, ok := pluginTable.RawGetString("description").(lua.LString); ok { if s, ok := pluginTable.RawGetString("description").(lua.LString); ok {
@@ -155,11 +156,11 @@ func (m *Manager) GetPlugins() []*Plugin {
return out return out
} }
func (m *Manager) TogglePlugin(name string) { func (m *Manager) TogglePlugin(id string) {
m.mu.RLock() m.mu.RLock()
var found *Plugin var found *Plugin
for _, p := range m.plugins { for _, p := range m.plugins {
if p.Name == name { if p.ID == id {
found = p found = p
break break
} }
@@ -173,8 +174,8 @@ func (m *Manager) TogglePlugin(name string) {
enabled := found.Enabled enabled := found.Enabled
found.mu.Unlock() found.mu.Unlock()
if m.pluginsFile != nil { if m.pluginsFile != nil {
if err := m.pluginsFile.setEnabled(name, enabled); err != nil { if err := m.pluginsFile.setEnabled(id, enabled); err != nil {
log.Printf("plugin %s: save state: %v", name, err) log.Printf("plugin %s: save state: %v", id, err)
} }
} }
if !enabled { if !enabled {
@@ -188,8 +189,8 @@ func (m *Manager) TogglePlugin(name string) {
if ret == lua.LFalse { if ret == lua.LFalse {
p.Enabled = false p.Enabled = false
if m.pluginsFile != nil { if m.pluginsFile != nil {
if err := m.pluginsFile.setEnabled(p.Name, false); err != nil { if err := m.pluginsFile.setEnabled(p.ID, false); err != nil {
log.Printf("plugin %s: save state: %v", p.Name, err) log.Printf("plugin %s: save state: %v", p.ID, err)
} }
} }
} }
@@ -217,11 +218,11 @@ func (m *Manager) TogglePlugin(name string) {
} }
} }
func (m *Manager) SaveConfig(name, configText string) { func (m *Manager) SaveConfig(id, configText string) {
m.mu.RLock() m.mu.RLock()
var found *Plugin var found *Plugin
for _, p := range m.plugins { for _, p := range m.plugins {
if p.Name == name { if p.ID == id {
found = p found = p
break break
} }
@@ -234,8 +235,8 @@ func (m *Manager) SaveConfig(name, configText string) {
found.ConfigText = configText found.ConfigText = configText
found.mu.Unlock() found.mu.Unlock()
if m.pluginsFile != nil { if m.pluginsFile != nil {
if err := m.pluginsFile.setConfig(name, configText); err != nil { if err := m.pluginsFile.setConfig(id, configText); err != nil {
log.Printf("plugin %s: save config: %v", name, err) log.Printf("plugin %s: save config: %v", id, err)
} }
} }
if _, ok := found.hooks["on_config"]; !ok { if _, ok := found.hooks["on_config"]; !ok {
@@ -243,7 +244,7 @@ func (m *Manager) SaveConfig(name, configText string) {
} }
found.mu.Lock() found.mu.Lock()
if _, err := callHook(found, "on_config"); err != nil { if _, err := callHook(found, "on_config"); err != nil {
log.Printf("plugin %s on_config: %v", name, err) log.Printf("plugin %s on_config: %v", id, err)
} }
found.mu.Unlock() found.mu.Unlock()
} }
@@ -273,8 +274,8 @@ func (m *Manager) RunOnStart() {
if ret == lua.LFalse { if ret == lua.LFalse {
p.Enabled = false p.Enabled = false
if m.pluginsFile != nil { if m.pluginsFile != nil {
if err := m.pluginsFile.setEnabled(p.Name, false); err != nil { if err := m.pluginsFile.setEnabled(p.ID, false); err != nil {
log.Printf("plugin %s: save state: %v", p.Name, err) log.Printf("plugin %s: save state: %v", p.ID, err)
} }
} }
} }
+32 -14
View File
@@ -9,7 +9,7 @@ import (
type pluginFileEntry struct { type pluginFileEntry struct {
Enable bool `yaml:"enable"` Enable bool `yaml:"enable"`
Config string `yaml:"config,omitempty"` Config interface{} `yaml:"config,omitempty"`
} }
type pluginsFileData struct { type pluginsFileData struct {
@@ -51,28 +51,46 @@ func (pf *PluginsFile) save() error {
return os.WriteFile(pf.path, raw, 0o600) return os.WriteFile(pf.path, raw, 0o600)
} }
func (pf *PluginsFile) get(name string) (enabled bool, config string, found bool) { func (pf *PluginsFile) get(id string) (enabled bool, config string, found bool) {
e, ok := pf.data.Plugins[name] e, ok := pf.data.Plugins[id]
return e.Enable, e.Config, ok if !ok {
return false, "", false
}
if e.Config == nil {
return e.Enable, "", true
}
raw, err := yaml.Marshal(e.Config)
if err != nil {
return e.Enable, "", true
}
return e.Enable, string(raw), true
} }
func (pf *PluginsFile) register(name string, defaultEnabled bool) { func (pf *PluginsFile) register(id string, defaultEnabled bool) {
if _, ok := pf.data.Plugins[name]; !ok { if _, ok := pf.data.Plugins[id]; !ok {
pf.data.Plugins[name] = pluginFileEntry{Enable: defaultEnabled} pf.data.Plugins[id] = pluginFileEntry{Enable: defaultEnabled}
_ = pf.save() _ = pf.save()
} }
} }
func (pf *PluginsFile) setEnabled(name string, enabled bool) error { func (pf *PluginsFile) setEnabled(id string, enabled bool) error {
e := pf.data.Plugins[name] e := pf.data.Plugins[id]
e.Enable = enabled e.Enable = enabled
pf.data.Plugins[name] = e pf.data.Plugins[id] = e
return pf.save() return pf.save()
} }
func (pf *PluginsFile) setConfig(name string, configText string) error { func (pf *PluginsFile) setConfig(id string, configText string) error {
e := pf.data.Plugins[name] e := pf.data.Plugins[id]
e.Config = configText if configText == "" {
pf.data.Plugins[name] = e e.Config = nil
} else {
var parsed interface{}
if err := yaml.Unmarshal([]byte(configText), &parsed); err != nil {
return err
}
e.Config = parsed
}
pf.data.Plugins[id] = e
return pf.save() return pf.save()
} }
+3
View File
@@ -11,6 +11,7 @@ type HookConfig struct {
} }
type Plugin struct { type Plugin struct {
ID string
Name string Name string
Description string Description string
FilePath string FilePath string
@@ -37,6 +38,7 @@ func (p *Plugin) HookConfig(name string) (HookConfig, bool) {
} }
type Info struct { type Info struct {
ID string
Name string Name string
Description string Description string
FilePath string FilePath string
@@ -57,6 +59,7 @@ func (p *Plugin) Info() Info {
hooks[k] = v hooks[k] = v
} }
return Info{ return Info{
ID: p.ID,
Name: p.Name, Name: p.Name,
Description: p.Description, Description: p.Description,
FilePath: p.FilePath, FilePath: p.FilePath,
+4 -4
View File
@@ -59,11 +59,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.textarea.Blur() m.textarea.Blur()
if info, ok := m.selected(); ok && m.manager != nil { if info, ok := m.selected(); ok && m.manager != nil {
val := m.textarea.Value() val := m.textarea.Value()
m.manager.SaveConfig(info.Name, val) m.manager.SaveConfig(info.ID, val)
// Update cached info. // Update cached info.
m.filtered[m.cursor].ConfigText = val m.filtered[m.cursor].ConfigText = val
for i := range m.items { for i := range m.items {
if m.items[i].Name == info.Name { if m.items[i].ID == info.ID {
m.items[i].ConfigText = val m.items[i].ConfigText = val
break break
} }
@@ -107,10 +107,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case key.Matches(msg, pk.Toggle): case key.Matches(msg, pk.Toggle):
if info, ok := m.selected(); ok && m.manager != nil { if info, ok := m.selected(); ok && m.manager != nil {
m.manager.TogglePlugin(info.Name) m.manager.TogglePlugin(info.ID)
m.filtered[m.cursor].Enabled = !info.Enabled m.filtered[m.cursor].Enabled = !info.Enabled
for i := range m.items { for i := range m.items {
if m.items[i].Name == info.Name { if m.items[i].ID == info.ID {
m.items[i].Enabled = !info.Enabled m.items[i].Enabled = !info.Enabled
break break
} }