28 KiB
Plugin Runtime
Описание системы плагинов Verstak Desktop: discovery, lifecycle, capabilities, states.
Plugin Discovery
Discovery Directories
Plugins ищутся в двух директориях (порядок приоритета):
| Путь | Назначение | Коммитится |
|---|---|---|
~/.config/verstak/plugins/ |
User-installed plugins | Нет (user home) |
./plugins/ |
Bundled / dev plugins | Нет (.gitignore) |
./plugins/ как Dev/Install Target
Директория ./plugins/ в корне verstak-desktop используется как:
- Dev target —
install-dev-plugins.shкоприрует сюда собранные пакеты изverstak-official-plugins/dist/. - Bundled plugins — при дистрибутиве core может поставлять плагины здесь.
Директория не коммитится. Каждый разработчик устанавливает плагины через install-dev-plugins.sh.
Discovery Process
DiscoverPlugins(dirs []string)сканирует каждую директорию.- Для каждой поддиректории читает
plugin.json. ValidateManifest()проверяет:schemaVersion == 1,id,name,version,apiVersion, минимум 1provides, минимум 1permissions.- Дубликаты
idотбрасываются с warning.
Подробнее о формате manifest см. Plugin Manifest Format.
Plugin Lifecycle States
discovered
│
├─ disabled — plugin.json найден, но plugin отключён
│
├─ loading — plugin начинает загрузку
│
├─ loaded — все required и optional capabilities разрешены
│
├─ degraded — required capabilities разрешены, но не хватает optional
│
├─ missing-required-capability — не хватает хотя бы одной required capability
│
├─ failed — ошибка при регистрации capabilities (дубликат, panic)
│
├─ incompatible — schemaVersion или apiVersion не поддерживаются
│
└─ (Скрыто) — discovered используется как промежуточный статус в discovery
Определения статусов
| Статус | Условие | Поведение |
|---|---|---|
discovered |
plugin.json прочитан и валиден | Промежуточный, до capability resolution |
disabled |
Plugin отключён пользователем | Не загружается |
loaded |
Все capabilities разрешены | Полная функциональность |
degraded |
Required OK, но не хватает optional | Работает, часть функций недоступна |
missing-required-capability |
Не хватает required capability | Не загружается, показать ошибку |
failed |
Ошибка регистрации capabilities | Не загружается |
incompatible |
Неподдерживаемая schemaVersion/apiVersion | Не загружается |
Required / Optional Capabilities
Правило
requires— жёсткая зависимость. Если ни один plugin не предоставляет требуемый capability, плагин получаетmissing-required-capabilityи не загружается.optionalRequires— мягкая зависимость. Если capability нет, плагин переходит вdegraded, но продолжает работать.
Регистрация core capabilities
Core capabilities регистрируются в main.go ДО plugin discovery:
coreCaps := []string{
"verstak/core/plugin-manager/v1",
"verstak/core/capability-registry/v1",
"verstak/core/contribution-registry/v1",
"verstak/core/permissions/v1",
"verstak/core/events/v1",
}
capRegistry.Register("verstak-desktop", coreCaps)
// Vault capability — регистрируется отдельно после vault initialization
capRegistry.Register("verstak-desktop", []string{"verstak/core/vault/v1"})
Это гарантирует, что любой plugin с requires: ["verstak/core/plugin-manager/v1"] будет загружен.
Plugin capability resolution
foreach plugin:
1. зарегистрировать plugin.provides в capability registry
2. проверить plugin.requires — если есть missing → missing-required-capability
3. проверить plugin.optionalRequires — если есть missing → degraded
4. иначе → loaded
5. зарегистрировать plugin.contributes в contribution registry
Capability конфликт
Два plugin не могут предоставлять один и тот же capability. При попытке повторной регистрации — ошибка и статус failed для второго плагина.
Plugin Manifest Format
Файл plugin.json в корне директории плагина.
Обязательные поля
| Поле | Тип | Описание |
|---|---|---|
schemaVersion |
int | Должен быть 1 |
id |
string | Уникальный идентификатор (regex: [a-zA-Z0-9.-]+) |
name |
string | Человекочитаемое имя |
version |
string | Semver (напр. "0.1.0") |
apiVersion |
string | API версии plugin |
provides |
string[] | Список capabilities (мин. 1) |
permissions |
string[] | Список запрашиваемых permissions (мин. 1) |
Опциональные поля
| Поле | Тип | Описание |
|---|---|---|
description |
string | Описание плагина |
source |
string | "official", "local", "third-party" |
icon |
string | Иконка (emoji или имя) |
requires |
string[] | Жёзкие capability-зависимости |
optionalRequires |
string[] | Мягкие capability-зависимости |
frontend |
object | { "entry": "path/to/index.js", "style": "path/to/style.css" } |
backend |
object | { "type": "go", "entry": { "linux-amd64": "...", ... }, "healthCheck": {...} } |
migrations |
object | { "path": "migrations/" } |
contributes |
object | UI contributions (см. ниже) |
sync |
object | { "namespaces": [...], "participate": bool } |
Пример
{
"schemaVersion": 1,
"id": "verstak.platform-test",
"name": "Platform Test",
"version": "0.1.0",
"apiVersion": "0.1.0",
"provides": ["verstak/platform-test/v1"],
"requires": ["verstak/core/plugin-manager/v1"],
"optionalRequires": ["verstak/core/vault/v1", "verstak/core/sync/v1"],
"permissions": ["vault.read", "events.publish", "ui.register"],
"frontend": { "entry": "frontend/dist/index.js" },
"contributes": {
"views": [{ "id": "my.view", "title": "My View", "component": "MyPanel" }],
"commands": [{ "id": "my.cmd", "title": "Run", "handler": "run" }]
}
}
Contribution Points
Плагины могут регистрировать UI contributions через поле contributes в plugin.json.
Реализованные contribution points (Milestone 5a)
| Тип | Поле manifest | Описание | Frontend host |
|---|---|---|---|
| Боковая панель | sidebarItems |
Элементы в sidebar слева | ✅ Sidebar.svelte (из ContributionRegistry) |
| Основные панели | views |
Полноценные страницы/панели | ✅ ViewContainer.svelte (PluginBundleHost — real frontend bundle) |
| Панели настроек | settingsPanels |
Панели в Plugin Manager | ✅ PluginManager.svelte (кнопка Settings, открывает modal) |
| Команды | commands |
Команды для command palette | ✅ ContributionRegistry (UI command palette не реализован) |
Планируемые contribution points
| Тип | Поле manifest | Статус |
|---|---|---|
| Действия над файлами | fileActions |
Registry готов, UI не реализован |
| Действия над заметками | noteActions |
Registry готов, UI не реализован |
| Контекстное меню | contextMenuEntries |
Registry готов, UI не реализован |
| Провайдеры поиска | searchProviders |
Registry готов, UI не реализован |
| Провайдеры активности | activityProviders |
Registry готов, UI не реализован |
| Элементы status bar | statusBarItems |
Registry готов, UI не реализован |
Структура contribution points в manifest
{
"contributes": {
"sidebarItems": [
{
"id": "mypanel.sidebar",
"title": "My Panel",
"icon": "📌",
"view": "mypanel.view",
"position": 100
}
],
"views": [
{
"id": "mypanel.view",
"title": "My Panel View",
"icon": "📌",
"component": "MyPanelComponent"
}
],
"settingsPanels": [
{
"id": "mypanel.settings",
"title": "My Settings",
"component": "MySettingsPanel"
}
],
"commands": [
{
"id": "mypanel.cmd",
"title": "Do Something",
"icon": "⚡",
"handler": "doSomething"
}
]
}
}
Contribution lifecycle
- Plugin
Register(pluginID, contributions)— все contributions регистрируются Unregister(pluginID)— удаляет все contributions указанного plugin- Reload:
Unregister → Register(предотвращает дублирование) - Disable plugin →
Unregister(contributions исчезают из UI) - Enable plugin →
Registerпри следующем Reload - Registry idempotent: Register удаляет старые записи перед добавлением новых
Error boundary
- Ошибка в plugin view/settings placeholder не роняет shell
- ViewContainer показывает "⚠️ Plugin UI failed" fallback
- Error канал:
console.error+ видимый fallback в UI
Frontend Bundle Contract
Регистрация компонентов
Плагин регистрирует frontend компоненты через глобальную функцию window.VerstakPluginRegister:
window.VerstakPluginRegister('plugin.id', {
components: {
'ComponentName': {
mount: function(containerEl, props, api) {
// containerEl — div, созданный PluginBundleHost
// api — ограниченный VerstakPluginAPI
containerEl.innerHTML = '<h1>Hello from plugin!</h1>';
},
unmount: function(containerEl) {
// Очистка при смене view
containerEl.innerHTML = '';
}
}
}
});
VerstakPluginAPI
API объект передаётся в mount() и содержит только ограниченный набор методов:
| Свойство | Статус | Описание |
|---|---|---|
api.pluginId |
✅ Работает | ID плагина |
api.capabilities.has(id) |
🔧 Stub | Запрос capability registry (planned) |
api.events.publish(type, payload) |
🔧 Stub | Публикация события (planned) |
api.events.subscribe(type, handler) |
🔧 Stub | Подписка на события (planned) |
api.settings.read(key) |
🔧 Stub | Чтение настроек плагина (planned) |
api.settings.write(key, value) |
🔧 Stub | Запись настроек плагина (planned) |
api.commands.execute(id, args) |
🔧 Stub | Выполнение команды (planned) |
Загрузка бандла
PluginBundleHostполучает pluginId и componentId- Вызывает
App.GetPluginFrontendInfo(pluginId)— получает entry/style/rootPath - Вызывает
App.GetPluginAssetContent(pluginId, entry)— получает JS контент - Выполняет контент через
new Function(content)— bundle вызываетVerstakPluginRegister - Находит компонент по componentId и вызывает
mount(container, props, api) - При смене view — вызывает
unmount(container)для старого компонента
Безопасность asset path
| Правило | Проверка |
|---|---|
| Нет абсолютных путей | Пути, начинающиеся с / или \, отклоняются |
| Нет path traversal | Пути, содержащие .., отклоняются |
| Нет выхода за root | После filepath.Join проверяется, что путь внутри plugin root |
| Только существующие файлы | os.ReadFile возвращает ошибку если файл не существует |
manifest frontend config
{
"frontend": {
"entry": "frontend/dist/index.js",
"style": "frontend/style.css"
}
}
Reload
ReloadPlugins() в internal/api/app.go позволяет перезагрузить plugins без перезапуска приложения:
- Unregister all non-core capabilities.
- Re-register core capabilities + vault + workspace (если открыт).
- Re-scan discovery directories.
- For each plugin: re-run capability resolution.
- Unregister contributions before re-registering (предотвращает дубли).
- Register contributions for loaded/degraded plugins (disabled/failed — не регистрируются).
- Update plugins list.
Frontend вызывает это при нажатии "Reload" в Plugin Manager.
Vault Core Capability
verstak/core/vault/v1— регистрируется вmain.goпосле остальных core capabilities, когда vault инициализирован.- Vault layout:
<base>/VerstakVault/.verstak/с подпапками (см. ниже). - Plugin namespace paths:
plugin-data/<id>,plugin-settings/<id>,plugin-cache/<id>. - Vault events:
vault.created,vault.opened,vault.closed,vault.error. - Vault status:
not-created,closed,open,error. - Path traversal protection через
ResolveSafePath.
Vault Directory Layout
<base>/
VerstakVault/ ← vault root (создаётся CreateVault)
.verstak/
vault.json ← VaultMeta (schemaVersion, vaultId, createdAt, app)
plugin-data/ ← per-plugin data namespaces
<plugin-id>/
plugin-settings/ ← per-plugin settings namespaces
<plugin-id>/
plugin-cache/ ← per-plugin cache namespaces
<plugin-id>/
trash/ ← soft-deleted items
logs/ ← vault-scoped logs
Vault API
| Метод | Описание |
|---|---|
CreateVault(path) |
Создаёт VerstakVault/ с .verstak/ layout и vault.json. Публикует vault.created. |
OpenVault(path) |
Открывает существующий vault, валидирует vault.json. Публикует vault.opened. |
CloseVault() |
Закрывает vault, сбрасывает path/meta. Публикует vault.closed. |
GetVaultStatus() |
Возвращает текущий статус: not-created, closed, open, error. |
GetVaultPath() |
Возвращает путь к vault root. |
GetVaultMeta() |
Возвращает VaultMeta (vaultId, schemaVersion, timestamps). |
ResolveSafePath(rel) |
Безопасно резолвит относительный путь внутри vault. Блокирует path traversal. |
GetPluginDataPath(id) |
Возвращает (и создаёт) plugin-data/<id>/. |
GetPluginSettingsPath(id) |
Возвращает (и создаёт) plugin-settings/<id>/. |
GetPluginCachePath(id) |
Возвращает (и создаёт) plugin-cache/<id>/. |
Vault Events
| Event | Когда публикуется | Payload |
|---|---|---|
vault.created |
После успешного CreateVault |
path, vaultId |
vault.opened |
После успешного OpenVault |
path, vaultId |
vault.closed |
После CloseVault |
vaultId |
vault.error |
При ошибках операций | error |
Vault Status Flow
not-created ──CreateVault──▶ open ──CloseVault──▶ closed
│ │
└──OpenVault─────────┘
Файлы реализации
| Файл | Назначение |
|---|---|
internal/core/plugin/plugin.go |
Manifest, ValidateManifest, DiscoverPlugins, Status |
internal/core/capability/registry.go |
CapabilityRegistry |
internal/core/contribution/registry.go |
ContributionRegistry |
internal/core/permissions/registry.go |
PermissionsRegistry |
internal/core/events/bus.go |
EventBus |
internal/api/app.go |
Wails API, ReloadPlugins |
internal/core/vault/vault.go |
Vault service: CreateVault, OpenVault, CloseVault, ResolveSafePath, plugin namespace paths |
internal/core/vault/vault_test.go |
|
internal/core/storage/api.go |
|
internal/core/storage/api_test.go |
|
internal/core/appsettings/manager.go |
|
internal/core/appsettings/manager_test.go |
|
internal/core/pluginstate/manager.go |
|
internal/core/pluginstate/manager_test.go |
App Settings
App settings хранятся локально (НЕ внутри vault) в ~/.config/verstak/config.json.
Поле | Назначение
---|---
currentVaultPath | Путь к текущему vault
recentVaults | Список недавних vault (max 10, без дублей)
theme | Тема (dark/light)
devMode | Режим разработки
userPluginsDir | Директория пользовательских плагинов
windowState | Состояние окна (размеры, максимизация)
lastOpenedAt | Время последнего запуска
Правила
- Если config отсутствует — создаётся с defaults
- Если config битый — backup + создание defaults с понятной ошибкой
currentVaultPathпри запуске проверяется и vault открывается автоматически- Secrets НЕ хранятся в app settings
Vault Plugin State
Vault plugin state хранится внутри vault в .verstak/plugins.json.
Структура
{
"schemaVersion": 1,
"enabledPlugins": ["verstak.platform-test"],
"disabledPlugins": [],
"desiredPlugins": [
{
"id": "verstak.platform-test",
"version": "0.1.0",
"source": "official"
}
],
"updatedAt": "2026-06-17T..."
}
Поле | Назначение
---|---
enabledPlugins | Плагины, которые активны в этом vault
disabledPlugins | Плагины, которые явно отключены
desiredPlugins | Плагины, которые нужны этому vault (для будущей синхронизации)
updatedAt | Время последнего обновления
Правила
- Enabled/disabled состояние относится к конкретному vault
- Disabled plugin не регистрирует provides/contributions
- Plugin settings остаются в
.verstak/plugin-settings/<id>/settings.json - Отсутствие
plugins.json→ создаётся с defaults - Битый
plugins.json→ backup + defaults с понятной ошибкой - App settings НЕ хранятся внутри vault
- Plugin packages НЕ хранятся в vault settings
Installed vs Enabled
- Installed — plugin package существует в discovery directory
- Enabled — plugin активен в vault plugin state
- Disabled — plugin установлен, но отключен в vault
- Missing installed — plugin listed в
desiredPlugins, но package отсутствует локально
Missing Installed Plugins
Состояние для будущей синхронизации:
desiredPluginsможет содержать plugin, которого нет локально- Plugin Manager показывает "Missing installed plugin"
- Auto-install пока НЕ делается
- Показывается подсказка: "Install official plugin package"
UI Layout
┌─────────────────────────────────────────────────────┐
│ App.svelte │
│ ┌──────────┬──────────────────────────────────────┐ │
│ │ Sidebar │ Content area │ │
│ │ │ │ │
│ │ Verstak │ PluginManager | ViewContainer │ │
│ │ │ │ │
│ │ 🧩 Plugin│ (padding: 1.5rem) │ │
│ │ Manager│ │ │
│ │ │ │ │
│ │ Plugins │ │ │
│ │ 📌 item1 │ │ │
│ │ 📌 item2 │ │ │
│ │ │ │ │
│ │ ● Vault │ │ │
│ └──────────┴──────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
- Sidebar (220px): навигация (Plugin Manager), plugin sidebar items, vault status
- Content: Plugin Manager или ViewContainer в зависимости от выбранного view
- Vault Selection: полноэкранный экран, показывается когда vault не открыт
Milestone 4b — UI Completion (2026-06-17)
Сделано:
- VaultSelection.svelte: исправлен flow (CreateVault → OpenVault → SetCurrentVault)
- Sidebar.svelte: полная навигация с отступами, plugin sidebar items, vault status
- App.svelte: обработка
verstak:navсобытий, global reset стилей - PluginManager.svelte: исправлены отступы header
Проверки:
go test ./...— 52 PASS./scripts/check.sh— ✅./scripts/smoke-platform.sh— ✅ (enable/disable/plugins.json)./scripts/build.sh— ✅
Workspace / Cases Core Capability
Workspace — центральная модель Верстака вокруг "дел". Это НЕ notes/files — это фундамент.
Ноды
| Поле | Тип | Описание |
|---|---|---|
id |
UUID | Стабильный идентификатор |
parentId |
string | ID родителя (пусто для root) |
type |
space/case/folder | Тип ноды |
title |
string | Название |
status |
active/sleeping/archived | Жизненный цикл |
tags |
string[] | Теги |
order |
int | Порядок среди siblings |
createdAt |
RFC3339Nano | Создан |
updatedAt |
RFC3339Nano | Обновлён |
Хранение
<vault>/.verstak/workspace.json — атомарная запись (temp + rename).
API
GetWorkspaceTree()— полное деревоCreateWorkspaceNode(parentID, type, title)— создатьRenameWorkspaceNode(id, title)— переименоватьMoveWorkspaceNode(id, newParentID)— переместитьArchiveWorkspaceNode(id)— архивироватьSetCurrentWorkspaceNode(id)— выбрать текущуюGetCurrentWorkspaceNode()— получить текущую
Capability
verstak/core/workspace/v1 — регистрируется только когда vault открыт и workspace инициализирован.
Правила
- Root node создаётся при создании vault
- Порядок children стабилен (sort by order)
- Нельзя переместить ноду в себя или в своего потомка
- Архивирование — soft delete (status = archived)
- Corrupt JSON → backup + defaults
Типы нод
| Тип | Назначение |
|---|---|
space |
Рабочее пространство (root) |
case |
Дело |
folder |
Папка |
НЕ добавляются: note, file, action, secret, worklog, link — это плагины.
Lifecycle Events
Planned (not yet implemented in runtime):
workspace.node.createdworkspace.node.renamedworkspace.node.movedworkspace.node.archivedworkspace.node.selectedworkspace.error
UI
WorkspaceTree в sidebar:
- Дерево с expand/collapse
- Создание case/folder
- Выбор текущей ноды
- Индикатор статуса (active/archived/sleeping)
Build Scripts
В verstak-desktop/scripts/ есть два скрипта:
build.sh — локальная детерминированная сборка
- Собирает только
verstak-desktop(core platform). - Не трогает другие репозитории.
- Fail-fast: любая ошибка (go vet, go test, frontend build, wails build) прерывает сборку.
- Проверяет: deps → frontend build → go mod download → go vet → go build → go test → wails build + plugin copy.
- Используется в CI и для повседневной работы над core.
update-and-build-all.sh — dev helper для полной пересборки связки
- Не для CI. Только для разработки, когда нужно быстро собрать всё вместе.
- Шаги:
git pull --ff-onlyво всех 6 репозиториях- Сборка official plugins (frontend npm build + backend go build для каждого плагина)
- Копирование собранных плагинов в
verstak-desktop/plugins/ - Запуск
build.shдля сборки desktop
- Ошибки pull и сборки плагинов не прерывают скрипт (best-effort), но ошибка build.sh прерывает (fail-fast).