Metadata-Version: 2.5
Name: mal-plugin-cli
Version: 0.1.2
Summary: Scaffold and publish MyAppsLibrary plugins from the terminal
Project-URL: Homepage, https://marketplace.rodolphe-augusto.fr
Project-URL: Repository, https://github.com/rodolphe37/my-apps-library-plugins-marketplace
Author: rodolphe37
License-Expression: MIT
Requires-Python: >=3.11
Requires-Dist: httpx>=0.27
Requires-Dist: rich>=13.7
Requires-Dist: typer>=0.12
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# mal-plugin — CLI for MyAppsLibrary plugins

Scaffold and publish [MyAppsLibrary](../../my-apps-library) plugins to the [marketplace](..) from the terminal — no browser needed except for the one-time GitHub sign-in.

```bash
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](../README.md)) — 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

```bash
pip install mal-plugin-cli   # or: pipx install mal-plugin-cli (recommended — isolated environment)
```

## Configuration

Two environment variables, both optional (defaults point at `localhost` for local development against this repo's own `backend`/`frontend`):

| Variable | Default | Purpose |
|---|---|---|
| `MAL_PLUGIN_API_BASE_URL` | `http://localhost:8000` | The marketplace backend to talk to |
| `MAL_PLUGIN_MARKETPLACE_URL` | `http://localhost:5173` | Used only to print the live plugin URL after `publish` |
| `MAL_PLUGIN_GITHUB_CLIENT_ID` | *(none)* | **Required for `login`** — see below |

### `mal-plugin login` requires a GitHub OAuth App with Device Flow enabled

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](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow)), but it does have to correspond to a real App:

1. [github.com/settings/developers](https://github.com/settings/developers) → **New 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 a one-time setup step, 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.

## 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

```bash
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):

```bash
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):

```bash
pytest
```

Lint:

```bash
ruff check mal_plugin tests
```

## Distribution

Published on PyPI as [`mal-plugin-cli`](https://pypi.org/project/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:

```bash
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
```
