mal-plugin — CLI for MyAppsLibrary plugins
Scaffold and publish MyAppsLibrary plugins to the marketplace from the terminal — no browser needed except for the one-time GitHub sign-in.
mal-plugin login # sign in with GitHub (Device Flow)
mal-plugin new # scaffold a plugin project (feature or translation)
cd my-plugin-id
mal-plugin publish # validate, package, submit, and wait for the scan result
Why a separate identity from the web session
An admin login is a wholly separate identity in this marketplace (see the root README) — the CLI isn't that, it's the same GitHub-authenticated developer identity the web app uses, just reached a different way. GitHub's OAuth Device Authorization Flow (the same mechanism gh auth login uses) lets the CLI sign in without a browser-embedded redirect or a locally-listening HTTP server: it shows a short code, you enter it at a URL GitHub gives you, and the CLI polls until you've approved it. The CLI then exchanges the resulting GitHub token for the marketplace's own long-lived bearer token (POST /api/auth/cli/exchange — see backend/app/routers/auth.py), stored locally and never touching a browser cookie.
That bearer token is visible and revocable from the web at any time — sign in on the marketplace site and go to Account → Active CLI sessions.
Install
pip install mal-plugin-cli # or: pipx install mal-plugin-cli (recommended — isolated environment)
Configuration
Nothing to configure — the published package's defaults already point at the real, hosted marketplace (api.marketplace.rodolphe-augusto.fr) and its own GitHub OAuth App (Device Flow). mal-plugin login works immediately after install, no environment variables needed.
Three environment variables exist for anyone pointing the CLI at a different backend instead — a self-hosted fork, or this repo's own local dev setup (see "Development setup" below):
| Variable | Default (published package) | Purpose |
|---|---|---|
MAL_PLUGIN_API_BASE_URL |
https://api.marketplace.rodolphe-augusto.fr |
The marketplace backend to talk to |
MAL_PLUGIN_MARKETPLACE_URL |
https://marketplace.rodolphe-augusto.fr |
Used only to print the live plugin URL after publish |
MAL_PLUGIN_GITHUB_CLIENT_ID |
the marketplace's own CLI OAuth App | A GitHub OAuth App with Device Flow enabled — see below |
Running against your own marketplace instance? You need your own GitHub OAuth App
A GitHub OAuth App's client id is not a secret (it's meant to be embedded in a public/native client like this one — see GitHub's Device Flow docs), but it does have to correspond to a real App your instance's backend also recognizes (the CLI exchanges its GitHub token for a marketplace session — see backend/app/routers/auth.py's /api/auth/cli/exchange). Only relevant if you're self-hosting a fork or developing this repo locally; the published CLI already has this covered for the real marketplace.
- github.com/settings/developers → New OAuth App. The callback URL field is irrelevant to Device Flow — anything valid works.
- In the App's settings, check "Enable Device Flow".
export MAL_PLUGIN_GITHUB_CLIENT_ID=<the App's Client ID>
This is independent of the backend's own GITHUB_CLIENT_ID/GITHUB_CLIENT_SECRET (used for the web Authorization Code flow, see backend/README.md) — the CLI never needs a client secret, since Device Flow's token exchange doesn't require one. Deliberately two separate Apps even for the same marketplace instance: a Device Flow App checked "Enable Device Flow" has no business also being the web login's Authorization Code App.
Commands
| Command | What it does |
|---|---|
mal-plugin login |
Sign in with GitHub (Device Flow) |
mal-plugin whoami |
Show the currently signed-in account |
mal-plugin logout |
Sign out and revoke this CLI session |
mal-plugin new [--dir PATH] |
Scaffold a new plugin project (prompts for type/id/name/description/category) |
mal-plugin publish [--dir PATH] |
Validate, package, submit, and wait for the scan result — requires mal-plugin login first |
Only publish requires being signed in (it's the only command that talks to an authenticated /api/dev/* endpoint) — new doesn't, since fetching categories and downloading a boilerplate are both public endpoints. You can scaffold a plugin, poke around, edit plugin.py, all before ever running login; you just won't be able to publish until you have.
new downloads the exact same boilerplate the web /tutorial//docs pages link to (GET /api/boilerplate/download) — there's a single source of truth for what a starter plugin looks like. It also writes a .mal-plugin.json sidecar file next to plugin.toml — marketplace-only bookkeeping (category, short description, the draft's id once created) kept out of plugin.toml itself, since that file's schema is the desktop app's contract, not the marketplace's.
publish runs a local pre-check (a hand-synced subset of the backend's real scan — see mal_plugin/validate.py's docstring) before ever making a network call, then walks the same steps the web wizard does: create-or-reuse draft → set presentation → upload version → submit → poll the automated scan to a result.
Publishing an update to an already-approved, live plugin works too — just bump plugin.toml's version and run mal-plugin publish again. The listing keeps serving the previous version to users until the new one passes review; a rejected update never takes a working plugin offline.
Writing the marketplace listing (description, screenshots, translations)
mal-plugin new scaffolds two extras beyond plugin.toml/plugin.py — neither ships in the plugin's own zip, both are read fresh by every publish run:
LONG_DESCRIPTION.md— the page's "About" section, rendered as Markdown. Write for someone who's never seen your plugin: what it does, exactly which menu it adds to (menu paths, dialog names — don't make anyone guess), how to use it.screenshots/— drop any.png/.jpg/.jpeg/.webpin here andpublishuploads them all, in filename order (01-...pngbefore02-...png), skipping ones already uploaded on a previous run. An optionalscreenshots/captions.json({"filename.png": "caption"}) sets captions; otherwise the filename is prettified into one.
Translations
Both are optional per locale, and neither is required to have the other:
- A translated long description: add
LONG_DESCRIPTION.<locale>.md(e.g.LONG_DESCRIPTION.fr.md) alongside the default one. - A translated short description: add it to
.mal-plugin.json'sshort_descriptionsdict, e.g.{"fr": "Une courte description."}.
A visitor browsing the marketplace in a language you didn't provide just sees your default-locale content — nothing is ever machine-translated.
Development setup
cd cli
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
Point it at your local backend (see ../backend/README.md for getting that running):
export MAL_PLUGIN_API_BASE_URL=http://localhost:8000
export MAL_PLUGIN_MARKETPLACE_URL=http://localhost:5173
Run the tests (no server or network needed — the API client and device flow are mocked):
pytest
Lint:
ruff check mal_plugin tests
Distribution
Published on PyPI as mal-plugin-cli — a separate, public registry unrelated to this repo's visibility (cli/'s own scoped pyproject.toml means only this directory's contents ever leave the repo, regardless of whether the monorepo itself is public or private).
To ship a new release, bump version in pyproject.toml, then:
cd cli
python -m build # generates dist/*.whl + dist/*.tar.gz
twine upload dist/* # requires a PyPI account + API token, scoped to this one project
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mal_plugin_cli-0.1.3.tar.gz.
File metadata
- Download URL: mal_plugin_cli-0.1.3.tar.gz
- Upload date:
- Size: 20.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6c1c4d3b0cd0468d400cf7f72f9403da57b6d5c5f800b939290cd7b7a061cdca
|
|
| MD5 |
aa51e2f6986251df0fcd912d96827c64
|
|
| BLAKE2b-256 |
0ef4bd4859783729d6988952fb16dd787e18066f0c58f08cc65246b31d806c8d
|
File details
Details for the file mal_plugin_cli-0.1.3-py3-none-any.whl.
File metadata
- Download URL: mal_plugin_cli-0.1.3-py3-none-any.whl
- Upload date:
- Size: 20.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e66fcd544912f458a107ac23d4c53a3d805848c224d163326949e32fa003e30d
|
|
| MD5 |
bf138eeac89fcf2f101cca3e163a38da
|
|
| BLAKE2b-256 |
c4d6c29f92996b51a9af0e5f65c58f3c0fe6b33e49c50e1e8b9ebb4f1d4c9709
|