CLI
Using the CLI
Install, authenticate, and script the myna CLI, including project linking, configuration resolution, and the JSON output contract.
The myna CLI is a full client for the Myna management API: schema push/pull, entries, assets, change sets, previews, and admin operations. It has two output personalities — human-readable tables by default, and a strict machine-readable mode behind --json — so the same binary serves interactive use, CI, and agent tooling. This page covers setup and behavior; the complete command list lives in the command reference.
Install
$ npm install --global @myna-sh/cli
$ myna --version
Or run it without installing:
$ npx @myna-sh/cli --help
Log in
$ myna login
myna login uses the browser device-authorization flow: the CLI prints a verification URL (app.myna.sh/device) and a one-time user code, opens the browser when running interactively, and polls until you approve. See authentication for how device flow relates to API keys.
The resulting credential is stored per API URL:
- macOS — the Keychain, under the service
sh.myna.cli. - Other platforms (or if the Keychain write fails) —
~/.config/myna/credentials.jsonwith0600permissions.
myna logout removes the stored credential for the current API URL; myna whoami validates it against the API and shows the identity. For automation, myna login start and myna login poll split the flow so nothing blocks on a human, and myna auth verify --json answers "does this credential work?" without throwing — see authentication.
Diagnose problems
myna doctor checks the whole chain in one pass: CLI version, API reachability and compatibility, credential validity, key scopes, project access, schema drift, generated-type freshness, and optionally a browser origin.
$ myna doctor --origin http://localhost:5173
✓ CLI version: 0.3.0 is current.
✓ API reachable: https://api.myna.sh speaks API 2026-07-24.
✓ Client compatibility: CLI 0.3.0 meets the minimum supported client 0.2.0.
✓ Credential: Valid — API key "site-agent" (4 scope(s)).
✓ Project: my-site.
! Local schema: 2 undeployed change(s) (additive).
→ myna schema diff, then myna schema push
✗ Browser origin: http://localhost:5173 is not allowed on my-site. A browser will discard responses from it.
→ myna projects update --origins https://my-site.com,http://localhost:5173
A check that cannot run reports skip with a reason; the remaining checks still run.
When the CLI version check reports you are behind, myna update performs the upgrade instead of leaving you the command:
$ myna update
Updating 0.9.0 → 0.10.0 with: pnpm add -g @myna-sh/[email protected]
Updated to 0.10.0. Run `myna doctor` to confirm the API agrees.
It resolves the latest release from npm, and runs the install with the package manager it detects from where the binary lives — printing the command first, so a wrong guess is visible rather than silently installing a second copy. --check reports without installing and exits 1 when an update exists, which makes it a CI gate. Running from a source checkout does nothing, because there is no release to move to.
It updates the CLI only. @myna-sh/sdk and @myna-sh/react ship in lockstep with it but belong to your project's manifest, so your package manager owns those.
--json emits { ok, failed, warned, checks: [{ id, title, status, detail, remedy }] } with status of pass, warn, fail, or skip. The exit code is 1 if any check fails, so it can gate CI.
To diagnose browser access on its own, myna cors check --origin <url> [--collection <key>] reports project, public API, collection visibility, and origin separately, each with its own fix.
Add and remove origins with myna projects origins list | add | remove. projects update --origins replaces the entire list.
Set up your coding agents
Two commands wire the machine's agents into Myna, so nobody has to transcribe a page of per-client JSON:
$ myna mcp install # write Myna's MCP server into every detected client
$ myna skills install # install the change-set, schema, and publishing rules
myna mcp install never writes a credential into a client's configuration file — those usually live in the repository — storing it in ~/.config/myna/mcp.json at 0600 instead, where @myna-sh/mcp already looks. myna mcp list shows which clients it detects and which already have Myna configured; myna mcp uninstall removes the entry. See MCP setup and agent skills.
Link a project
Most commands need a project. Link the current directory once instead of passing --project everywhere:
$ myna link my-project
$ myna status
myna link validates the project against the API and writes .myna/project.json containing the project, organization, and (if non-default) API URL — never credentials, so the file is safe to commit. The CLI finds the link by walking up from the working directory, like .git. myna unlink removes it; myna init scaffolds a local myna/ schema directory and links in one step when a project is already resolvable.
Configuration resolution
Every configuration value is resolved independently, first match wins:
| Precedence | Source | Values |
|---|---|---|
| 1 | Flags | --api-url, --token, --project, --organization |
| 2 | Environment | MYNA_API_URL, MYNA_TOKEN, MYNA_PROJECT, MYNA_ORGANIZATION |
| 3 | Linked project | .myna/project.json (found by walking up; no secrets) |
| 4 | User config | ~/.config/myna/config.json (apiUrl, defaultProject, defaultOrganization) |
The token has its own chain: --token → MYNA_TOKEN → the stored credential (Keychain or credentials file) for the resolved API URL. ~/.config/myna respects XDG_CONFIG_HOME.
Global flags — --json, --project, --organization, --token, --api-url, --no-interactive — may appear anywhere in the command line; the CLI hoists them before parsing, so myna entries list posts --json and myna --json entries list posts are equivalent.
The JSON output contract
With --json, the CLI prints exactly one JSON value on stdout and nothing else there. Diagnostics (progress notes, hints) go to stderr in human mode and are suppressed entirely in JSON mode. The document is versioned so scripts can detect shape changes:
{"version": 1, "data": {"apiUrl": "https://api.myna.sh", "credentialPresent": true, "credentialSource": "stored", "project": "my-project", "organization": "my-org", "linkedRoot": "/home/me/site"}}
myna status reads local configuration only and makes no network call, so it reports whether a credential is present, never whether it works. Use myna auth verify or myna doctor for that.
Errors are also a single JSON document — on stdout, not stderr — mirroring the API's problem shape:
{"version": 1, "error": {"code": "VALIDATION_FAILED", "status": 422, "title": "Validation failed", "detail": "Entry fields failed validation.", "requestId": "req_01j...", "fields": [{"path": "title", "message": "Required."}]}}
Exit codes are strict:
| Code | Meaning |
|---|---|
0 |
Success |
1 |
Operational error (API error, not found, timeout) |
2 |
Usage error (bad arguments, missing consequence flag) |
One deliberate exception: myna changes validate <id> exits 1 when the change set is invalid, even though the command itself succeeded — so CI can gate on validation directly.
Non-interactive and CI usage
Interactive behavior (prompts, opening the browser) is enabled only when stdout is a TTY and --no-interactive is not set; in a pipeline the CLI is automatically non-interactive. For CI:
$ export MYNA_TOKEN="$MYNA_AGENT_KEY" # a narrowly scoped API key from a secret store
$ myna --json --no-interactive entries list posts --project my-project
Use a scoped API key (see scopes) rather than a personal device-flow credential. Destructive operations never prompt; they require explicit consequence flags and fail with exit code 2 without them:
| Flag | Command |
|---|---|
--confirm-delete |
entries delete, assets delete |
--confirm-publish |
changes publish |
--allow-destructive |
schema push when the diff removes or breaks data |
All mutations automatically carry an Idempotency-Key header (a fresh UUID per call via the SDK), so retrying a failed pipeline step is safe on the server side.
Scripting patterns
The single-value JSON contract composes directly with jq:
$ myna --json changes list --status open | jq -r '.data.data[].id'
$ id=$(myna --json changes create --set title="Autumn refresh" | jq -r '.data.id')
$ myna --json changes validate "$id" || echo "invalid"
Check .error to branch on failures without parsing stderr:
$ out=$(myna --json entries get posts/welcome) || true
$ echo "$out" | jq -e '.error' >/dev/null && echo "failed: $(echo "$out" | jq -r '.error.code')"
Providing input: --data and --set
Write commands accept a JSON body via --data, as inline JSON or @file. It is the entry's fields, at the top level — there is no fields wrapper:
$ myna entries create posts --data '{"title": "Hello", "body": "..."}'
$ myna entries create posts --data @entry.json
--data must be a JSON object. --set path=value layers individual assignments on top (applied after --data), with dot paths and JSON-parsed values — anything that fails to parse as JSON is treated as a string:
$ myna entries update posts/welcome \
--set title="New title" \
--set featured=true \
--set tags='["news","release"]' \
--set seo.description="Compare Myna's plans."
Here featured=true becomes a boolean and the tags value becomes an array; title stays a string. Dot paths reach inside object fields, so seo.description sets one key of the seo object rather than replacing it.
The escape hatch: myna api
The commands above are a curated surface, which means there is always something they do not wrap yet — a route that shipped before its command, a query parameter nobody needed twice, a response field to read once. myna api calls the management API directly rather than leaving you to hand-roll curl:
$ myna api /projects/:project/entries?collection=posts
$ myna api POST /projects/:project/change-sets --data '{"title": "Autumn refresh"}'
$ myna api DELETE /projects/:project/previews/:preview
The method is optional when it is GET, so the common case reads like a path. The path is version-relative and a leading /v1 is accepted and stripped, so anything you copy out of these docs works unmodified.
It is a passthrough, not a wrapper: the API's response envelope is printed whole, { data, pagination } and all. What it does not make you reimplement is everything around the call — the credential chain, retries, the --json contract, RFC 9457 error rendering, and the same exit codes as every other command.
| Flag | Effect |
|---|---|
--data <json> |
Request body, inline or @file. Any JSON value, not just an object. Rejected on GET, HEAD, and OPTIONS. |
--header <name:value> |
An extra request header. Repeatable. |
--idempotency-key <key> |
Send Idempotency-Key. Unlike the wrapped commands, myna api does not generate one: sending a key makes the request eligible for automatic retry, and only you know whether replaying this particular call is safe. |
--include |
Also report the response status and headers on stderr, leaving stdout to the body. |
$ myna --json api /projects/my-site/releases | jq -r '.data.data[].releaseNumber'
