Skip to main content

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.

  1. github.com/settings/developersNew OAuth App. The callback URL field is irrelevant to Device Flow — anything valid works.
  2. In the App's settings, check "Enable Device Flow".
  3. 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/.webp in here and publish uploads them all, in filename order (01-...png before 02-...png), skipping ones already uploaded on a previous run. An optional screenshots/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's short_descriptions dict, 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

mal_plugin_cli-0.1.3.tar.gz (20.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mal_plugin_cli-0.1.3-py3-none-any.whl (20.7 kB view details)

Uploaded Python 3

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

Hashes for mal_plugin_cli-0.1.3.tar.gz
Algorithm Hash digest
SHA256 6c1c4d3b0cd0468d400cf7f72f9403da57b6d5c5f800b939290cd7b7a061cdca
MD5 aa51e2f6986251df0fcd912d96827c64
BLAKE2b-256 0ef4bd4859783729d6988952fb16dd787e18066f0c58f08cc65246b31d806c8d

See more details on using hashes here.

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

Hashes for mal_plugin_cli-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 e66fcd544912f458a107ac23d4c53a3d805848c224d163326949e32fa003e30d
MD5 bf138eeac89fcf2f101cca3e163a38da
BLAKE2b-256 c4d6c29f92996b51a9af0e5f65c58f3c0fe6b33e49c50e1e8b9ebb4f1d4c9709

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.3 This release

2 files

0.1.2

2 files

0.1.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page