Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/first-version.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ First version of `meili`, the Meilisearch CLI built for coding agents.
- Indexes: `meili get indexes [--id <index>]`; `meili add indexes --id <index> [--primary-key <field>]`, `meili update indexes --id <index> [--primary-key <field>] [--new-id <index>]`, which renames the index, and `meili delete indexes --id <index> --yes`.
- Tasks: `meili get tasks`, narrowed by `--ids`, `--statuses`, `--types`, `--index-ids`, `--batch-ids`, `--canceled-by` and the date filters of the route, paging with `--limit` and `--from` and following the `next` cursor with `--all`; `meili cancel tasks` and `meili delete tasks --yes`, which take the same filters and answer a task.
- Search: `meili search --index <index> --q <query>`, on `POST /indexes/{index_uid}/search`, with every non-experimental search parameter of the route as a flag, from `--filter` and `--sort` to `--hybrid` and `--vector`; `--data` or `--file` carries a whole search body, which a flag of the same field overrides.
- Cloud projects: `meili get projects [--id <id>]`, `meili add projects --name <name> --team-id <id> --resource-id <id>`, `meili update projects --id <id> --name <name>` and `meili delete projects --id <id> --yes`, the first commands of the Management API, reached with `--management-token` and `--management-url` or with the session of `meili auth login`. Its errors are passed through like Meilisearch's, same shape, same exit codes.
- `meili schema` prints the index of the command tree as JSON, one entry per command with its routes, and `meili schema --command "get indexes"` prints one command in full, with its key actions and whether it returns a task.
- Connection through `--project-url` and `--project-api-key`, or `MEILI_PROJECT_URL` and `MEILI_PROJECT_API_KEY`; `MEILI_MANAGEMENT_TOKEN` and `MEILI_MANAGEMENT_URL` for the Management API.
- Several projects in a config file, `~/.config/meili/config.json` or `MEILI_CONFIG`, chosen with `--project` on the command line and never deduced; `meili config list`, `config set` and `config unset` manage the file.
- A project's `cloudId` in the file gives its id to the management commands, `meili update projects --project prod`; `meili config sync-with-cloud` fills it from Meilisearch Cloud on the entries that have the project's URL, and `meili add projects --add-to-config` adds the project it creates, with the master key creation returns once.
- `meili auth login` logs in to Meilisearch Cloud with a browser, through the OAuth2 authorization code flow with PKCE on a loopback redirect, and stores the session in `credentials.json` next to the config file, mode `600`; the management commands refresh it on their own, `meili auth status` says whether that session is still accepted by the server, `meili auth refresh` rotates it on demand, and `meili auth logout` revokes it and forgets it.
- A command of the verb `delete` requires `--yes` on the command line: without it the command refuses to run, `cli_usage` and exit 2, before any network call. The CLI still never asks a question.
- `json` output by default, `ndjson` for lists, `--all` to follow the pagination, `--wait` and `--timeout` for asynchronous tasks.
Expand Down
12 changes: 6 additions & 6 deletions .claude/skills/add-command/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
---
name: add-command
description: Add exactly one command to the meili CLI (for example `get stats` or `cancel tasks`) from its routes in the OpenAPI document, following the naming rules of docs/contract/commands.md and CLAUDE.md, with its snapshot tests, integration test, manifest and changeset. Use it whenever a command must be added or when someone asks to expose an API route.
description: Add exactly one command to the meili CLI (for example `get stats` or `cancel tasks`) from its routes in the OpenAPI document, following the naming rules of docs/contract/commands.md and CLAUDE.md, with its snapshot tests, its integration test for a Meilisearch command, manifest and changeset. Use it whenever a command must be added or when someone asks to expose an API route.
argument-hint: <command, for example: get stats>
---

# Add the command `meili $ARGUMENTS`

You ship exactly one command in this change: `meili $ARGUMENTS`, with every route of its verb and resource. One file for the command, one test file, one integration test, the regenerated manifest, one changeset line. Nothing else, unless a shared piece of `src/lib/` is missing for this kind of command (say so in the report).
You ship exactly one command in this change: `meili $ARGUMENTS`, with every route of its verb and resource. One file for the command, one test file, one integration test for a Meilisearch command, the regenerated manifest, one changeset line. Nothing else, unless a shared piece of `src/lib/` is missing for this kind of command (say so in the report).

When `$ARGUMENTS` names several commands of the same resource, `get add update delete documents`, rule 10 of `CLAUDE.md` allows them in one change: run every step below once per command, and ship them complete or not at all. Commands of different resources stay in different changes.

Expand All @@ -15,7 +15,7 @@ Work through the steps in order. Every check below is enforced by `pnpm test` th
## 1. Read before you write

- `CLAUDE.md`, fully. The non-negotiable rules are the review checklist.
- `docs/contract/commands.md`: the grammar, the eleven naming rules, the exceptions table, the shared flag groups and the commands without an API.
- `docs/contract/commands.md`: the grammar, the eleven naming rules, the exceptions table, the shared flag groups and the commands that do not derive from a route.
- `docs/contract/output.md`: output modes, pass-through, errors, exit codes, asynchronous operations, destructive commands, input, pagination. The sections that match your routes are the specification of your command.
- `docs/contract/testing.md`: what the three test levels must contain.
- `docs/adr/`: skim the titles, read the ones your change touches. The contract says what the CLI promises; the ADRs say what was already decided, and which alternatives were refused and why. Contradicting one is a new ADR, not a silent change.
Expand All @@ -35,7 +35,7 @@ Compare the derived command with `$ARGUMENTS`:
- different name because the user's request contradicts the rules for no such reason: tell the user, propose the derived name, and follow what they decide; if they keep their name, it is an exception with their reason.
- no route in the document, or a parameter the user mentions that the document does not have: stop and ask the user. Do not guess a route and do not read it in Meilisearch's source code.
- a route behind an experimental feature: it is out of the contract (rule 10). Stop and tell the user; opening it is an exception they decide.
- a command that calls no API (`schema`): it must be in the "Commands without an API" table of `commands.md`; if it is not, stop and ask.
- a command that does not derive from a route (`schema`, the `config` commands): it must be in the "Commands that do not derive from a route" table of `commands.md`; if it is not, stop and ask.

From the routes, write down the flags of each: the path flags (rule 4: `--id` for the resource itself, the singular collection for a parent, `--index`), the query parameters and body fields (rule 6), the identifier flags named by what they identify and their API-spelling aliases (rule 5), and which flag combination selects which route (rule 3).

Expand All @@ -47,7 +47,7 @@ The command id is the file path. `meili get stats` gives:
|---|---|
| command | `src/commands/get/stats.ts` |
| unit test | `test/commands/get/stats.test.ts` |
| integration test | `test/integration/meilisearch/get-stats.test.ts` |
| integration test, Meilisearch commands only | `test/integration/meilisearch/get-stats.test.ts` |
| snapshot (generated) | `test/commands/get/__snapshots__/stats.test.ts.snap` |

The class name is the command id in PascalCase (`GetStats`). The first command of a new verb (`add`, `update`, `delete`, `cancel`, ...) also needs the topic description in `package.json`, under `oclif.topics.<verb>.description`, otherwise `meili --help` does not list the verb. A verb-only command (`search`, `export`) is a file at the root of `src/commands/`.
Expand Down Expand Up @@ -93,7 +93,7 @@ Run `pnpm test` once to write the snapshots, then read the `.snap` file: the JSO

## 5. Integration test

Copy `test/integration/meilisearch/get-health.test.ts` or `get-indexes.test.ts`. The file starts with `const meilisearchUp = await meilisearchAvailable()` and wraps everything in `describe.skipIf(!meilisearchUp)`. It runs the built binary with `runBinary([...])` against `MEILISEARCH_URL` and `MASTER_KEY` from `setup.ts`, never against the shell's `MEILI_PROJECT_URL`. Fixtures (an index, documents) are created with the SDK under a random suffix and deleted in `afterAll`, so the suite can run in parallel and never depends on existing data. A management command needs `test/integration/management/`, whose bootstrap (`docs/contract/testing.md`, section 2) does not exist yet: stop and tell the user it must be built first, in its own change.
Copy `test/integration/meilisearch/get-health.test.ts` or `get-indexes.test.ts`. The file starts with `const meilisearchUp = await meilisearchAvailable()` and wraps everything in `describe.skipIf(!meilisearchUp)`. It runs the built binary with `runBinary([...])` against `MEILISEARCH_URL` and `MASTER_KEY` from `setup.ts`, never against the shell's `MEILI_PROJECT_URL`. Fixtures (an index, documents) are created with the SDK under a random suffix and deleted in `afterAll`, so the suite can run in parallel and never depends on existing data. A management command has no integration test (`docs/contract/testing.md`, section 2): its handlers in `test/helpers/management-handlers.ts` follow `openapi/management.json`, and what the document does not say is reported to the user as a documentation gap, never guessed.

## 6. Manifest, changeset

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ jobs:
run: pnpm openapi
env:
MEILI_BOT_GH_PAT: ${{ secrets.MEILI_BOT_GH_PAT }}
- run: pnpm gen:management
- name: The committed Management API client must match the document
run: git diff --exit-code src/lib/clients/management/generated
- run: pnpm lint
- run: pnpm typecheck
- run: pnpm gen:manifest
Expand Down
10 changes: 5 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ Package: `@meilisearch/cli` on npm. Binary: `meili`.
- TypeScript, strict mode, `noUncheckedIndexedAccess` on. Node 22 or newer.
- oclif v5 (`@oclif/core` 5) for the command tree, topic separator is a space (`meili get indexes`). See ADR 0001.
- `meilisearch-js` for the Meilisearch API.
- `@hey-api/openapi-ts` generates the Management API client from `openapi/management.json`, downloaded by `pnpm openapi:management` and never committed, into `src/lib/clients/management/generated/`. Never edit generated files. Run `pnpm gen:management` to refresh them (the generator and the script arrive with the first management command). In code, commands, flags and environment variables, `management` is the name for everything that touches the Management API. `Cloud` only appears in prose, as the product name.
- vitest for tests, with snapshots. `msw` mocks HTTP in unit tests. Integration tests run against a local Meilisearch and a local Management API, see `docs/contract/testing.md`.
- `@hey-api/openapi-ts` generates the Management API client from `openapi/management.json`, downloaded by `pnpm openapi:management` and never committed, into `src/lib/clients/management/generated/`. Never edit generated files. Run `pnpm gen:management` to refresh them: it runs the generator through `pnpm dlx` with TypeScript 5 beside it, which the generator needs, from `openapi-ts.config.mjs`, and CI fails when the committed files differ. In code, flags and environment variables, `management` names the Management API itself, `--management-token`; `cloud` names the product, Meilisearch Cloud, where a command or a field speaks of it, `config sync-with-cloud`, `cloudId`.
- vitest for tests, with snapshots. `msw` mocks HTTP in unit tests. Integration tests run against a local Meilisearch; the Management API has none, see `docs/contract/testing.md`.
- Biome for lint and format.
- changesets for versioning and the changelog.
- pnpm as the package manager.
Expand All @@ -63,7 +63,7 @@ Use the skill: `/add-command <command>`, for example `/add-command get stats`. I
4. Call the client. Read commands end with `await this.output(result)`. Write commands end with `await this.outputTask(task, flags)`.
5. Map errors. Do nothing for API errors, `BaseCommand` maps them. Add a `hint` only when you know the next command the user should run.
6. Create `test/commands/<same path>.test.ts` from the sibling test. It must snapshot the `--help` output and the JSON output for one success case and one error case, with `msw` mocks.
7. Add one integration test in `test/integration/meilisearch/` or `test/integration/management/`. Meilisearch runs locally from the `docker compose` file of this repository. The Management API needs the bootstrap described in `docs/contract/testing.md`, which does not exist yet: build it in its own pull request before the first management command. Unit tests still mock HTTP with `msw`; integration tests hit the real thing.
7. For a Meilisearch command, add one integration test in `test/integration/meilisearch/`: Meilisearch runs locally from the `docker compose` file of this repository. A management command has none: Meilisearch Cloud does not run locally, its OpenAPI document is the source of truth, and the unit tests mock it from that document (`docs/contract/testing.md`). What the document does not say is a gap to report to the Cloud team, not something to guess.
8. Run `pnpm gen:manifest` so `oclif.manifest.json` and `meili schema` know the command.
9. Add the command to the changeset. Until the first release, `.changeset/first-version.md` is the only one and describes everything: add your line to it. After it, write a new file in `.changeset/` by hand with type `minor`: `pnpm changeset` is interactive.
10. Run the "Before you finish" list.
Expand Down Expand Up @@ -95,9 +95,9 @@ Then read the diff of your pull request twice. Once for simplicity: in Claude Co

## Where to read more

- `docs/contract/commands.md`: the naming rules that turn an API route into a command, the exceptions, the global and shared flags, the commands without an API
- `docs/contract/commands.md`: the naming rules that turn an API route into a command, the exceptions, the global and shared flags, the commands that do not derive from a route
- `docs/contract/output.md`: output modes, error format, exit codes, async tasks, pagination
- `docs/contract/config.md`: projects, the config file, connection resolution, environment variables, secrets
- `docs/contract/config.md`: projects, the config file, connection resolution, Cloud projects, environment variables, secrets
- `openapi/`: the OpenAPI documents downloaded by `pnpm openapi`, the reference for every command name
- `docs/contract/testing.md`: test levels, how the two local APIs start, what runs in pull request CI
- `docs/adr/`: the decisions and the reasons
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export MEILI_PROJECT_API_KEY=masterKey # only if the instance has a master key
./bin/run.js get indexes --debug # HTTP trace on stderr, keys masked
```

The management commands (not shipped yet) will read `MEILI_MANAGEMENT_TOKEN`, and `MEILI_MANAGEMENT_URL` to target something else than production.
The management commands read `MEILI_MANAGEMENT_TOKEN`, and `MEILI_MANAGEMENT_URL` to target something else than production; `pnpm openapi:management` needs a GitHub token that reads `meilisearch/meilisearch-cloud`, and `pnpm gen:management` regenerates the client from the document.

`pnpm link --global` puts a `meili` command on your PATH that points at your checkout.

Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ export MEILI_PROJECT_API_KEY=masterKey # only if the instance has a master key
meili get health
```

**Management API**, for the commands that manage Meilisearch Cloud, `meili get projects` or `meili add teams` (not shipped yet):
**Management API**, for the commands that manage Meilisearch Cloud, `meili get projects`, `meili add projects`, `meili update projects` and `meili delete projects`:

```sh
export MEILI_MANAGEMENT_TOKEN=... # a personal access token from the Meilisearch Cloud dashboard

meili add projects --name demo --team-id 12 --resource-id 3
```

The URL, the key and the token also exist as flags with the same name: `--project-url`, `--project-api-key`, and `--management-token` with the management commands. The flag wins over the variable.
Expand All @@ -31,17 +33,20 @@ The URL, the key and the token also exist as flags with the same name: `--projec
{
"projects": {
"local": { "url": "http://localhost:7700" },
"prod": { "url": "https://ms-abc.sfo.meilisearch.io", "apiKey": "..." }
"prod": { "url": "https://ms-abc.sfo.meilisearch.io", "apiKey": "...", "cloudId": 343 }
}
}
```

```sh
meili get indexes --project prod
meili update projects --project prod --name renamed # a management command uses the cloudId
```

With several projects in the file, `--project` is required: the CLI never guesses, and it refuses to run when the file and the `MEILI_PROJECT_URL` variable both define a project. `MEILI_CONFIG` names another file. For a coding agent, give it its own file through `MEILI_CONFIG`, read-only: the file is shared state, and an agent must not be able to change where another one's commands go. The file holds your keys: keep it readable by you alone, the CLI warns on stderr otherwise, and an agent changes nothing in it, neither a project nor its permissions, unless you asked for it explicitly. `meili config list`, `meili config set` and `meili config unset` manage the file.

`cloudId` is the project's id in Meilisearch Cloud, which the dashboard does not show. `meili config sync-with-cloud` fills it on the entries whose URL is a project your token sees; `meili add projects --add-to-config` adds the project it creates, with its URL, its master key and its `cloudId`: creation is the one moment the key is returned, `sync-with-cloud` never writes one.

## Documentation

```sh
Expand Down
1 change: 1 addition & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"!oclif.manifest.json",
"!**/__snapshots__",
"!openapi",
"!src/lib/clients/management/generated",
"!CHANGELOG.md"
]
},
Expand Down
Loading
Loading