Skip to content

The plugin system

Every tool in PocketArk is a plugin: one directory is one plugin, with frontend and backend side by side. plugin.json is the single source of truth.

plugins/<plugin-id>/
├── plugin.json # manifest (single source of truth; shared by both scanners)
├── README.md # feature notes (for users)
├── AGENTS.md # dev conventions (for AI / maintainers, optional)
├── frontend/ # frontend
│ ├── views/ # tool pages (one PascalCase.vue per tools[] entry)
│ ├── settings/ # settings panel (optional, declared in plugin.json)
│ ├── components/ # plugin-private components (optional; a feature with ≥2 → components/<feature>/)
│ ├── composables/ # composables (optional, useXxx.ts)
│ ├── lib/ # pure logic TS (optional, no Vue dependency)
│ ├── shared.ts # shared constants / types (optional)
│ ├── schema.ts # database tables (optional, imported by code)
│ └── setup.ts # plugin lifecycle hooks (optional, build-scanned)
└── backend/ # backend (optional)
├── mod.rs # #[tauri::command] commands (**only here**)
├── migrations.rs # migration aggregation: exports `pub fn all()` (optional)
├── <feature>/ # multi-file feature (≥2 files): <feature>/mod.rs + other .rs
└── <single>.rs # single-file feature / asset

Hard rules

  • The frontend/ top level only allows the whitelist: views/ settings/ components/ composables/ lib/ shared.ts schema.ts setup.ts. Everything else lives in those directories or lib/.
  • Pure logic (no Vue dependency) goes in frontend/lib/; use frontend/composables/ only when reactivity / lifecycle is needed.
  • Naming: components PascalCase.vue; composables useXxx.ts; other TS kebab-case.ts; Rust files snake_case.rs; feature directories use a singular lowercase noun.
  • Commands live only in backend/mod.rs (build.rs scans only that file); other .rs files are its submodules.
  • A pure-frontend plugin may ship only frontend/, but plugin.json / README.md / AGENTS.md are still required.
  • Directories starting with _ or . are not registered (e.g. the _template).

Machine checks live in scripts/lint-plugins.mjs (R-P1~R-P6), wired into pnpm lint; for a genuine exception, write lint-plugins-ignore in the file with a reason.

Adding a plugin requires no registration in any framework file:

Scan pointLocationPurpose
Manifestplugins/*/plugin.jsontools / settings / icons / ordering
Tool pagesplugins/*/frontend/views/*.vuetargeted by manifest.tools[].entry
Settings panelplugins/*/frontend/settings/*.vuetargeted by manifest.settings.entry
Frontend hooksplugins/*/frontend/setup.tsplugin initialization
Backend commandsplugins/*/backend/mod.rsbuild.rs parses #[tauri::command] and generates the registry
Migrationsplugins/*/backend/migrations.rsaggregated into all(), scoped per version

Navigation, routing, titles, enable/disable toggles, the settings panel, the error boundary and KeepAlive all work automatically.

ItemRule
Plugin idGlobally unique, kebab-case, equal to the directory name; route /tool/:id
Tool idtools[].id, globally unique; prefer <plugin-id> or <plugin-id>-<feature>
Rust commandFunction name = frontend call name, prefixed with <plugin_id>_
Migrationmigration(scope, version, ...) with scope = plugin id, version starting at 1 per scope
  • Plugin group order = the smallest order in the group.
  • Tool order within a group = order * 1000 + index in the tools array.
  • Default order is 100, then alphabetical by directory name.
  • plugins/_template/: the minimal skeleton (used by the scaffolder), demonstrating the directory conventions and page structure.
  • plugins/hello-world/: the full path (commands, logging, database, forms, tables, tasks, notifications, multi-window).
  • plugins/system/: a pure-frontend plugin (no backend, just tables and dialogs).

Next: Create a plugin and the plugin.json manifest.