40 KiB
Plugin Runtime
Описание системы плагинов Verstak Desktop: discovery, lifecycle, capabilities, states.
Plugin Discovery
Discovery Directories
Plugins ищутся через единый resolver internal/core/plugin.ResolveDiscoveryDirs.
Порядок приоритета:
| Путь | Назначение | Коммитится |
|---|---|---|
VERSTAK_PLUGIN_DIR |
Override для тестов/dev; можно передать несколько путей через OS path separator | Нет |
./plugins/ |
Dev plugins относительно текущей рабочей директории/repo | Нет (.gitignore) |
<binary-dir>/plugins/ |
Packaged plugins рядом с desktop binary | Зависит от дистрибутива |
~/.config/verstak/plugins/ |
User-installed plugins | Нет (user home) |
Resolver нормализует пути, удаляет дубликаты и передает discovery только канонический список директорий. Отсутствующие директории просто пропускаются на этапе scanning.
Discovery сканирует все resolved директории в указанном порядке. Если один и тот
же plugin.id найден несколько раз, применяется правило first plugin wins:
первый найденный plugin загружается, последующие plugins с тем же id пропускаются.
Конфликт логируется и возвращается как discovery warning с двумя путями: путь
пропущенного duplicate и путь уже загруженного winner.
./plugins/ как Dev/Install Target
Директория ./plugins/ от текущей рабочей директории используется как:
- Dev target —
install-dev-plugins.shкоприрует сюда собранные пакеты изverstak-official-plugins/dist/. - Local override — при запуске desktop из repo позволяет быстро проверять packaged bundles.
В packaged-сборке bundled plugins должны лежать в plugins/ рядом с executable.
Для тестов и локальных сценариев можно задать VERSTAK_PLUGIN_DIR=/path/to/plugins.
Директория не коммитится. Каждый разработчик устанавливает плагины через 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",
"verstak/core/files/v1",
"verstak/core/workbench/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", "workbench.open"],
"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 не реализован) |
| Open/edit providers | openProviders |
Провайдеры viewer/editor для Workbench routing | ✅ ContributionRegistry + минимальный Workbench host |
Планируемые 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 удаляет старые записи перед добавлением новых
Bundled Frontend Plugin API
Bundled frontend plugins получают API от host через createPluginAPI(pluginId).
Обычный plugin code не передает pluginId в методы API: scope закрепляется в
host при mount компонента. Это защищает нормальный cooperative path от случайного
доступа к чужому namespace.
Текущая модель безопасности честно ограничена:
- bundled frontend plugins исполняются в общем JS-контексте приложения;
- проверки permissions/capabilities сейчас являются contract/policy checks, а не полноценной security boundary;
- malicious JS в общем контексте теоретически может обойти frontend wrapper;
- настоящая изоляция будет только после отдельного sidecar/sandbox milestone.
Workbench Open/Edit Routing
Files and Notes plugins do not import or embed a concrete editor plugin. They
call api.workbench.openResource(request) or api.workbench.editResource(request).
The backend requires the source plugin to be enabled, loaded/degraded, and to
declare workbench.open. This is a policy/contract check, not a security
boundary.
OpenResourceRequest:
type OpenResourceRequest = {
kind: "vault-file";
path: string;
mode?: "view" | "edit";
mime?: string;
extension?: string;
context?: {
sourcePluginId?: string;
sourceView?: "files" | "notes" | string;
isInsideNotesFolder?: boolean;
notesScopePath?: string;
notesMode?: boolean;
};
};
Routing contexts are fixed as generic-text, generic-markdown, and
notes-markdown. .md/.markdown inside canonical Notes/ folders uses
notes-markdown; markdown outside Notes uses generic-markdown; ordinary text
uses generic-text. Milestone 6b derives context from request fields; future
Files/Notes integrations can centralize canonical Notes folder auto-detection in
the Workbench helper.
contributes.openProviders extends the existing contribution registry:
{
"contributes": {
"openProviders": [
{
"id": "verstak.platform-test.markdown-diagnostic",
"title": "Platform Test Markdown Diagnostic",
"priority": 100,
"component": "MarkdownDiagnosticProvider",
"supports": [
{
"kind": "vault-file",
"extensions": [".md", ".markdown"],
"contexts": ["generic-markdown", "notes-markdown"]
},
{
"kind": "vault-file",
"mime": ["text/plain"],
"extensions": [".txt", ".log"],
"contexts": ["generic-text"]
}
]
}
]
}
}
Selection uses enabled loaded/degraded provider plugins, resource kind,
extension/mime, context, user preference, priority, then deterministic
pluginId/providerId fallback. If nothing matches, Workbench returns
status: "no-provider" and shows the fallback view instead of a core editor.
Disabled/failed/missing-required-capability plugins are excluded from provider
selection at request time by activeOpenProviders(). Their contributions may
remain in the registry until the next ReloadPlugins() cycle, but they never
match during routing.
Draft app-global preferences are defaultTextEditorProvider,
defaultMarkdownEditorProvider, and defaultNotesMarkdownEditorProvider.
Vault-scoped and per-extension overrides are deferred.
Default Editor Plugin
The official verstak.default-editor plugin (verstak-official-plugins/plugins/default-editor/)
provides openProviders for text, generic markdown, and notes-context markdown files.
It uses api.files.readText / api.files.writeText for file I/O and mounts through
the standard PluginBundleHost / provider host mechanism. Core does not import or
reference this plugin directly.
Provider plugins may have no sidebar item — openProviders are contribution points for workbench routing, not navigation. Plugin Manager displays openProviders in the contributions summary.
API methods
settings
settings.read()— читает весь settings namespace текущего plugin.settings.read(key)— читает один ключ.settings.write(key, value)— обновляет один ключ и пишет namespace обратно.settings.writeAll(settings)— заменяет settings namespace.- Backend требует plugin exists, enabled, status
loaded/degradedи permissionstorage.namespace.
capabilities
capabilities.list()— возвращает текущий capability registry.capabilities.get(name)— возвращает{ available, name, pluginId, status }.capabilities.has(name)— boolean wrapper надget.- Backend требует, чтобы plugin был enabled/loaded и декларировал dependency на
verstak/core/capability-registry/v1вrequiresилиoptionalRequires.
commands
commands.register(commandId, handler)— регистрирует bundled frontend handler. ВозвращаетPromise<unsubscribe>.commands.execute(commandId, args)— backend сначала проверяет plugin status, permissioncommands.registerи что command объявлен вcontributes.commandsименно этим plugin. Затем frontend registry вызывает зарегистрированный handler.- Если command объявлен в manifest, но handler не зарегистрирован, API возвращает
понятную ошибку
declared-but-unhandled. - Handler registry очищается при component unmount, reload/disable flow и
api.dispose().
events
events.subscribe(eventName, handler)— frontend-local subscription с backend validation permissionevents.subscribe. ВозвращаетPromise<unsubscribe>.events.publish(eventName, payload)— backend проверяетevents.publish, затем событие dispatch'ится в bundled frontend event bus.- Handler получает envelope
{ name, pluginId, payload, timestamp }. - Subscriptions очищаются при component unmount, reload/disable flow и
api.dispose().
files
files.list(relativeDir)— list directory using a vault-relative path.files.metadata(relativePath)— returns file/folder/symlink metadata.files.readText(relativePath)— reads a UTF-8 regular file, with a size limit.files.writeText(relativePath, content, options)— atomically writes text via temp-file-and-rename.options.createIfMissingandoptions.overwritecontrol conflicts.files.createFolder(relativePath)— creates one folder when the parent exists.files.move(from, to, options)— moves a file or folder; rejects moving a folder into itself and conflicts unlessoptions.overwriteis true.files.trash(relativePath)— moves a file/folder into internal.verstak/trash/files/<trashId>/...and returns trash metadata.- Backend requires plugin exists, enabled, status
loaded/degraded, open vault, andfiles.read,files.write, orfiles.delete. - All paths are canonical vault-relative slash paths. Backslashes, POSIX
absolute paths, Windows drive paths, UNC/network paths,
.., null bytes, symlink traversal, and public access to.verstak/are rejected. .verstakis reserved case-insensitively:.verstak,.Verstak, and any first path segment with that spelling are internal-only.files.metadatamay report a final symlink astype: "symlink", butfiles.listthrough a symlink directory and all read/write/move/trash operations through symlinks are forbidden in Milestone 6a.- Files API is text-only for read/write in Milestone 6a.
readTextis limited to UTF-8 regular files up to 2 MB. Binary streaming, watcher, restore, external editor integration, and Files UI plugin are deferred.
dispose
dispose()вызывается host'ом при cleanup. Plugin code обычно не вызывает его напрямую. Он удаляет зарегистрированные command handlers и event subscriptions.
Runtime boundaries
| Layer | Current status |
|---|---|
| Bundled frontend runtime | Functional for settings, capabilities, commands, events and text Files API |
| Backend validation | Checks plugin exists, enabled/loaded state, permissions and declarations |
| Security boundary | Not implemented; bundled plugins share the desktop frontend JS context |
| Sidecar/RPC/sandbox | Not implemented |
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() и содержит plugin-scoped методы текущего
bundled runtime. Это реальный runtime contract для cooperative bundled plugins,
но не sandbox/security boundary.
| Свойство | Статус | Описание |
|---|---|---|
api.pluginId |
✅ Работает | ID плагина |
api.settings.read(key?) |
✅ Работает | Читает plugin-scoped settings через backend bridge |
api.settings.write(key, value) |
✅ Работает | Пишет один settings key через backend bridge |
api.settings.writeAll(settings) |
✅ Работает | Заменяет settings namespace плагина |
api.capabilities.list() |
✅ Работает | Возвращает capability registry |
api.capabilities.get(id) |
✅ Работает | Возвращает capability entry/status |
api.capabilities.has(id) |
✅ Работает | Boolean wrapper над get |
api.commands.register(id, handler) |
✅ Работает | Регистрирует bundled frontend handler для объявленной command |
api.commands.execute(id, args) |
✅ Работает | Валидирует declaration/permission/backend state и вызывает bundled handler |
api.events.publish(type, payload) |
✅ Работает | Валидирует permission и публикует во frontend event bus |
api.events.subscribe(type, handler) |
✅ Работает | Валидирует permission и подписывает handler на frontend event bus |
api.files.list(relativeDir) |
✅ Работает | Список vault-relative директории, .verstak скрыта |
api.files.metadata(relativePath) |
✅ Работает | Metadata для файла/папки/symlink без чтения содержимого |
api.files.readText(relativePath) |
✅ Работает | Читает UTF-8 regular file до 2 MB |
api.files.writeText(relativePath, content, options) |
✅ Работает | Atomic text write с явным create/overwrite policy |
api.files.createFolder(relativePath) |
✅ Работает | Создаёт vault-relative folder |
api.files.move(from, to, options) |
✅ Работает | Move file/folder с conflict и path-policy checks |
api.files.trash(relativePath) |
✅ Работает | Перемещает в internal trash, permanent delete нет |
api.workbench.openResource(request) |
✅ Работает | Routes vault resources to openProviders |
api.workbench.editResource(request) |
✅ Работает | Same routing, forcing mode: "edit" |
api.dispose() |
✅ Работает | Очищает command handlers и event subscriptions текущего API instance |
Ограничения:
- permissions/capabilities checks являются contract/policy checks;
- bundled frontend plugins исполняются в общем JS-контексте;
- malicious JS не изолирован;
- sidecar process lifecycle, RPC transport и sandbox enforcement ещё не реализованы.
- Files paths are slash-only vault-relative contract paths; backslashes,
Windows absolute paths, UNC paths,
.verstakvariants, traversal and symlink operations are rejected by backend policy checks.
Загрузка бандла
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).