# Publishing a plugin

Once your plugin works locally, you can submit it to the **Onda plugin catalog** so other users discover it from inside the app (Settings → Plugins → Browse) and from the public catalog at [onda.mindfullabai.dev/plugins](https://onda.mindfullabai.dev/plugins).

This page covers the full path: manifest requirements, account / token, bundling, the upload endpoint, and what happens after submission.

## TL;DR

```bash
# 1. Make sure manifest.json has a reverse-DNS id (e.g. dev.you.your-plugin)
# 2. Get your publish token from your account (see "Token" below)
export ONDA_PUBLISH_TOKEN=onda_pk_xxxxxxxxxxxx

# 3. Publish (uses the onda-publish CLI shipped with onda-electron)
cd path/to/your/plugin    # directory containing manifest.json + main.js + ...
onda-publish

# → POST /api/plugins/publish, returns { plugin_id, version, status: 'pending' }
# Wait for review; once approved it appears in the catalog.
```

## Before you submit

The submit endpoint enforces a manifest schema. Get these right or the upload returns `422 MANIFEST_INVALID_SCHEMA`.

### Plugin id format (mandatory)

`manifest.id` must be a **reverse-DNS slug** with 3+ dot-separated parts, lowercase letters / digits / dashes only:

```
✓ sh.onda.pomodoro              # built-in style (reserved for Onda team)
✓ dev.you.my-plugin             # personal third-party
✓ com.acme.team-radar           # org third-party
✓ io.github.handle.tool         # GitHub-handle scoped

✗ team-panel                    # too few parts, NOT accepted
✗ My_Plugin                     # uppercase + underscore, NOT accepted
✗ org.author                    # only 2 parts, NOT accepted
```

Regex enforced server-side: `^[a-z0-9-]+(\.[a-z0-9-]+){2,}$`.

The id is **forever** — you can publish new versions but the id can't change. Pick carefully.

### Required manifest fields

```jsonc
{
  "id": "dev.you.my-plugin",       // see above
  "name": "My Plugin",             // 2-80 chars, shown in UI
  "version": "0.1.0",              // semver (X.Y.Z, optional -pre suffix)
  "main": "main.js",               // entry script (relative to bundle root)

  // Optional but recommended:
  "description": "What your plugin does",   // ≤ 500 chars
  "homepage": "https://github.com/you/my-plugin",
  "icon_url": "https://raw.githubusercontent.com/you/my-plugin/main/icon.png",
  "changelog": "What changed in this version",  // ≤ 5000 chars
  "author": { "name": "Your Name", "url": "https://you.dev" },

  // Capabilities the plugin requests at install time — users see them in the
  // install dialog and can revoke per-permission later in Settings.
  "capabilities": [
    "commands",
    "panel",
    "filesystem:read",
    "notifications"
  ]
}
```

See [manifest.md](./manifest.md) for the full field reference and [capabilities.md](./capabilities.md) for the list of permissions you can request.

### Version semantics

- New `version` per submission. The endpoint rejects `409 VERSION_DUPLICATE` if the `(id, version)` pair already exists.
- Bumps follow semver: `1.0.0` → `1.0.1` for fixes, `1.1.0` for features, `2.0.0` for breaking manifest changes (new required capability, dropped command, etc.).
- Pre-releases allowed: `1.0.0-beta.1`. They're shown only to users who opt into beta in Settings.

## Token

Publishing requires a bearer token tied to your Onda account.

### Getting your token

1. Sign in to [onda.mindfullabai.dev](https://onda.mindfullabai.dev) (GitHub OAuth)
2. Open [/account/tokens](https://onda.mindfullabai.dev/account/tokens)
3. Click **Create publish token**, name it (e.g. `laptop`), copy the value — it's shown **once**, store it in your password manager or env file.

The token grants two scopes:
- `publish` — POST new plugin versions you own.
- `mine` — list plugins / versions you've submitted.

Tokens never expire, but you can revoke them anytime from `/account/tokens`. Revoke + recreate if a machine is lost.

### Storing the token

```bash
# In your shell profile (~/.zshrc or ~/.bashrc):
export ONDA_PUBLISH_TOKEN="onda_pk_xxxxxxxxxxxx"

# Or per-project (.env file, gitignored):
ONDA_PUBLISH_TOKEN=onda_pk_xxxxxxxxxxxx
```

Never commit tokens. The endpoint rate-limits to **10 publishes per hour per token**.

## The bundle

Your plugin ships as a `.zip` containing the manifest + everything `main.js` needs to run.

### Layout

```
my-plugin/
├── manifest.json         # required, at the root of the zip
├── main.js               # the entry script declared by manifest.main
├── README.md             # optional, shown in the catalog detail page
├── icon.png              # optional, displayed if no icon_url
└── lib/                  # optional, any extra files main.js loads
    └── helper.js
```

### Rules

- **5 MB hard cap** on the zip. Trim dependencies (no `node_modules`, no source maps unless you really need them).
- The plugin runs inside a **Web Worker**. No Node APIs, no DOM. See [api-reference.md](./api-reference.md) for the surface available via the `onda.*` global.
- `manifest.json` **must be at the zip root** (not inside a subfolder). When zipping, run `zip -r my-plugin.zip *` from inside the plugin dir, not `zip -r my-plugin.zip my-plugin/`.

### Creating the zip manually

```bash
cd path/to/my-plugin
zip -r ../my-plugin.zip . -x "*.DS_Store" -x ".git/*" -x "node_modules/*"
```

Or use the `onda-publish` CLI (next section), which handles this for you.

## Submitting — `onda-publish` CLI

The Onda app ships an `onda-publish` script in its CLI bundle. It zips the current directory, validates the manifest locally, and uploads to the catalog.

### Install

The CLI is included with Onda. After installing the app, run once:

```bash
# From Onda app: Settings → CLI → Install onda CLI to PATH
# Or manually:
ln -s /Applications/Onda.app/Contents/Resources/dist/cli/onda /usr/local/bin/onda
ln -s /Applications/Onda.app/Contents/Resources/dist/cli/onda-publish /usr/local/bin/onda-publish
```

Verify: `onda-publish --version` should print the CLI version.

### Usage

```bash
cd path/to/my-plugin
onda-publish                     # publish current directory
onda-publish --dry-run           # validate + show what would be sent, no upload
onda-publish --token onda_pk_... # override env token
onda-publish --endpoint https://staging.onda.sh/api/plugins/publish
```

Flags:
- `--dry-run` — local manifest + bundle validation, no network call. Safe to run anywhere.
- `--token <token>` — override `$ONDA_PUBLISH_TOKEN`.
- `--endpoint <url>` — override the default `https://onda.mindfullabai.dev/api/plugins/publish` (useful for staging).
- `--bundle <path>` — supply a pre-built zip instead of zipping the current dir.

### Manual cURL (if you can't or don't want to use the CLI)

```bash
zip -r /tmp/my-plugin.zip . -x "*.DS_Store" -x ".git/*"

curl -X POST https://onda.mindfullabai.dev/api/plugins/publish \
  -H "Authorization: Bearer $ONDA_PUBLISH_TOKEN" \
  -F "manifest=$(cat manifest.json);type=application/json" \
  -F "bundle=@/tmp/my-plugin.zip;type=application/zip"
```

Success response:
```json
{
  "ok": true,
  "plugin_id": "dev.you.my-plugin",
  "version": "0.1.0",
  "status": "pending",
  "bundle_sha256": "ab12...",
  "bundle_size_bytes": 28341,
  "message": "Publish accepted, pending review"
}
```

## After submission — review and approval

A submission lands in status `pending`. It is **not** visible in the catalog until an Onda maintainer promotes it to `approved`.

What the review checks:
1. **Bundle is what the manifest claims** — no hidden network calls, no exec capability if not declared, no obfuscation that hides the real surface.
2. **Capabilities are actually used** — if you ask for `terminal:write` but never call it, we'll ask you to drop it.
3. **The plugin runs without throwing** in a clean Onda install (smoke test).
4. **Description matches behavior** — no "calculator" plugin that's actually a crypto miner.

Most reviews land within 48 hours for first-party submissions, longer (3-7 days) for unknown authors. You'll get an email at the address on your account when status changes.

### Status lifecycle

```
pending  → approved          (published live in catalog)
pending  → rejected (reason) (you receive an email, you can fix + resubmit a new version)
approved → suspended         (security issue found post-approval; users are notified)
```

You can check status anytime at `/account/plugins` or via `onda-publish mine`.

## Publishing a new version

Identical flow — same plugin id, new `version` string in the manifest. Old versions remain installable (the catalog defaults to `latest` but `Install specific version` is exposed in the detail page).

Bump version in `manifest.json`, update `changelog`, re-run `onda-publish`. You don't need to "claim" the id again — the endpoint verifies ownership by token.

## FAQ

**Can I publish to a private catalog (only my team)?**
Not in v1. The catalog is public. For team-private plugins, share the zip directly and have users sideload via Settings → Plugins → Install from file.

**Can I include native binaries (`.node`, `.so`)?**
No. Plugins run in Web Workers; native code wouldn't load anyway. If your plugin needs to spawn processes, use the `exec` capability with a tight `execPatterns` allowlist.

**My plugin needs a specific Onda version. How do I declare it?**
Set `engineVersion` in the manifest (semver range): `"engineVersion": ">=1.9.0"`. Users on older versions see "Requires Onda 1.9.0 or newer" in the catalog and can't install.

**Can I transfer ownership of a plugin to another user?**
Email [hello@onda.sh](mailto:hello@onda.sh) from both accounts (current owner + new owner). Ownership transfer is a manual operation.

**What does the bundle SHA-256 in the response do?**
It's the integrity hash recorded in the catalog. When users install your plugin, Onda re-hashes the downloaded zip and refuses to extract if it doesn't match — protects against CDN tampering. You don't need to verify it yourself; the endpoint computes it server-side.

**My publish returns `409 PLUGIN_ID_TAKEN`. Can I claim it?**
Either someone else already published a plugin with that id (pick a different one), or two of your machines tried to claim it simultaneously (retry once, the second call will succeed under your token).
