Local Testing & Troubleshooting
Sideload a PuntoSuite plugin into the desktop app to test it before publishing, and diagnose the most common load, permission, capability, and contribution errors.
This page shows how to run your plugin inside the real desktop app before you publish it, plus a table of the errors you are most likely to hit and how to fix each one.
If you have not built a plugin yet, start with Developer quickstart. For the manifest fields referenced below, see Manifest; for permissions vs. capabilities, see Security.
Test locally before publishing
Run your built plugin in the desktop to test it. Either way it installs as an unverified sideload (trust tier C) that is fail-closed: it never auto-runs — you confirm it before it loads.
Option A — Developer mode (recommended)
In the desktop app, open Marketplace → Installed, turn on Developer mode, click Load plugin
from file…, pick your <id>-<version>.zip, and accept the unverified-plugin warning. It installs and
then prompts you to confirm it before it loads. Developer mode is off by default and only
unlocks local (non-marketplace) installs — a normal user never needs it, and the host re-checks it
(you can't bypass the gate from the UI). Marketplace installs never require it.
Option B — Copy it in manually
1. Build
Bundle src/index.ts to an ESM file at the entry path declared in your manifest (e.g. dist/index.js).
pnpm build
2. Copy into the desktop plugin folder
The desktop app scans discovery roots for subfolders that contain a manifest.json. In development the only root is <userData>/plugins, which on Windows is:
%APPDATA%\BodegaSuite\plugins\<manifest.id>\
Copy your manifest.json and your built dist/ folder into a directory named after your plugin's id:
# from your plugin project root (Git Bash / POSIX shell)
DEST="$APPDATA/BodegaSuite/plugins/com.example.my-plugin"
mkdir -p "$DEST"
cp manifest.json "$DEST/"
cp -r dist "$DEST/"
The folder name does not have to match the id, but matching it keeps things tidy. Discovery keys on the manifest's id, not the folder name.
3. Restart and confirm the sideload
Restart the desktop app, open Settings > Plugins, find your plugin, and confirm the sideload. Until you do, it stays discovered-but-not-loaded. After confirming, the host loads it and calls your activate(api) (see Lifecycle).
Discovery roots and merge order
The roots the desktop shell scans depend on the build (from resolveDesktopPluginRoots):
| Build | Roots scanned, in order |
|---|---|
| Development | <userData>/plugins only |
| Production | <resourcesPath>/plugins (packaged), then <userData>/plugins |
Notes confirmed against the discovery code:
- Later roots override earlier ones for the same
id— a plugin in<userData>/pluginsoverrides a packaged one with the same id (user overrides packaged). - A missing root is ignored (no error); the packaged root is empty unless IT bundles plugins.
packages/plugin-*and the repoplugins/directory are source/mirror only — they are NOT auto-loaded. Always sideload into<userData>/plugins.- Each root is scanned for immediate subfolders containing a
manifest.json; folders without one are skipped silently.
Common errors
Discovery validates each manifest.json and records a diagnostic instead of crashing. The categories below map to the diagnostics emitted by discovery (invalid_manifest, read_failed, incompatible_host, incompatible_api) and to runtime errors raised at load, in the BodegaAPI RPC, or by the contribution registry.
| Symptom | Cause | Fix |
|---|---|---|
Invalid / oversized manifest (invalid_manifest) | manifest.json fails pluginManifestSchema — bad id (must be reverse-DNS, lowercase, 1–214 chars, filesystem-safe), missing name/version, or an unknown permission token. (Note: the schema does not structurally check semver, and it accepts org.bodegasuite.* ids — that namespace is rejected later, at publish time, not at load.) | Read the Zod error in the plugin diagnostics. Fix the offending field; only declare known permissions (ipc:products:read, ipc:sales:read). See Manifest. |
Manifest cannot be read (read_failed) | The file is malformed JSON, unreadable, or the root directory could not be listed. | Validate the JSON (no trailing commas/comments). Confirm the file is named exactly manifest.json and is readable. |
| Entry file missing | entry points to a file that was not copied (e.g. dist/index.js absent), or entry is an absolute/traversal path (rejected by schema). | Build first, then copy the whole dist/ folder next to manifest.json. entry must be a relative POSIX path with no ... |
Incompatible host (incompatible_host) | bodegasuite.version constraint is not satisfied by the running host version. | Widen the constraint (e.g. ">=0.1.0") or update the desktop app. |
Incompatible API (incompatible_api) | bodegasuite.apiVersion is not a host-supported value. | Target a supported apiVersion (currently "v1"). |
| Unknown permission token | A string in permissions[] is not recognized (only ipc:products:read and ipc:sales:read exist today). | Remove or correct the token. Unknown tokens fail manifest validation, so the plugin will not load at all. |
Permission denied at db.* | You called db.products.* / db.sales.* without the matching permission. | Add ipc:products:read (for db.products.*) or ipc:sales:read (for db.sales.*) to permissions[], then rebuild and re-copy. See Hooks & API. |
Capability denied (Capability denied: plugin manifest does not list "<specifier>") | You imported a Node built-in (e.g. node:fs, node:net, node:child_process) without declaring the matching capabilities entry. Non-bundled plugins are deny-all by default. | Declare only what you need in capabilities (net/fs/child_process/worker); for net, also list hosts in net.allow. Remember: capabilities gate Node built-ins, permissions gate the BodegaAPI — they are different. See Security. |
| Plugin not loading | It is a sideload (tier C) that has not been confirmed. A dropped folder never auto-runs. | Open Settings > Plugins and confirm the sideload. Also confirm you copied into <userData>/plugins, not packages/plugin-* or the repo plugins/ dir. |
| Plugin disappeared after working before | It may be on the signed Ed25519 blocklist (kill switch, checked every boot), or its activate/hook callback threw ~5 consecutive times and was auto-disabled. | Check the plugin's status/diagnostics in Settings > Plugins. Fix the throwing callback; re-enable. A blocklisted plugin cannot be loaded. |
activate timeout | activate(api) never resolved (an unawaited async path, an infinite loop, or a hung await). | Make activate resolve promptly. Do setup work, register your disposers, and return; do not block on long-running work inside activate. See Lifecycle. |
| Contribution rejected — cap exceeded | More than 32 panels or 16 themes per plugin, or a duplicate panel id. | Stay within the caps; reuse one panel/theme instead of registering many. |
| Contribution rejected — charset / length | A panel/theme id, icon, or theme slug contains characters outside [a-zA-Z0-9._-], or a field exceeds its limit (panel id ≤ 64, title ≤ 120, icon ≤ 40, theme slug ≤ 64). | Use only letters, digits, ., _, -, and keep fields within the limits. The error message names the offending field and limit. |
Where errors surface
- Manifest/discovery problems (the
*_manifest,read_failed,incompatible_*rows) are recorded as discovery diagnostics — the plugin is skipped, not crashed, and the reason is available in Settings > Plugins. activate/db.*/capability/contribution problems surface at load time: in-process they reject inside youractivate(); for worker-isolated plugins they appear as a contained host warning (the worker is not allowed to take down the host).
Quick checklist before you publish
pnpm buildproduced the file named inentry.manifest.jsonvalidates: known permissions only, semverversion, non-reservedid.- You declared
capabilities(andnet.allow) for every Node built-in you import — nothing more. - You sideloaded into
<userData>/plugins/<id>/and confirmed it in Settings > Plugins. - Your
activatereturns promptly and tracks every disposer fordeactivate(see Lifecycle).
When local sideloading works end to end, move on to Publishing to package the ZIP and submit it to the developer portal.