= The rhc command line :nav-title: rhc CLI :description: Configure a service from a file in git: collections, indexes, permissions, plugins and settings, applied the same way from a terminal or a CI pipeline. :keywords: CLI, infrastructure as code, configure backend from git, CI pipeline, rhc setup, deploy backend configuration :group: Tools :order: 10 NOTE: `rhc` ships with the 9.8 release of the RESTHeart Cloud kit. `rhc` configures a service from a file committed to git — collections, indexes, permissions, plugins and their settings — applied the same way from a terminal or a CI pipeline. [source,bash] ---- npm install -g @restheart-cloud/cli rhc login rhc setup --srv f3a9c1 --dry-run rhc setup --srv f3a9c1 ---- == Why a file and not the console The console is the right tool for looking around and for one-off changes. It is the wrong one for knowledge you need twice. A service's configuration is a handful of settings that must line up — an ACL that lets guests read a catalog, a plugin's success URL matching a route in the app, a collection name the client also knows. Written down as a checklist, it is performed once by hand and nothing checks it afterwards. Written as a setup file it is runnable, re-runnable, diffable when it changes, and it lives in the same commit as the code it configures. == Logging in The credential is a xref:tokens.adoc[personal access token]. Issue one at https://cloud.restheart.com/me/tokens. [source,bash] ---- rhc login # prompts for the token; stored 0600 under ~/.config/restheart rhc logout # forgets it here — revoke it in the console to kill it everywhere ---- `rhc` has no way to accept a password, and that is the design rather than a gap. See xref:tokens.adoc#personal-access-tokens[why] — briefly: an OAuth user has none, and an account password is far too wide a credential to hand a pipeline. `rhc login` checks the token against the admin node before storing it. Writing an unverified credential to disk only moves the failure to the next command, where it lands in the middle of something you cared about. == Commands and options [cols="1,3"] |=== | Command | What it does | `rhc login` | Store a personal access token for later commands. | `rhc logout` | Forget the stored token. | `rhc setup` | Bring a service to the state the setup file describes. |=== [cols="1,3"] |=== | Option | Meaning | `--srv ` | The service to set up. Required. | `--file ` | A module exporting a setup — a default export, or one named `setup`. A function export is called with no arguments. Defaults to `./rhc.setup.ts`. | `--dry-run` | Run every check, apply nothing, write nothing. | `--force ` | Apply the steps whose name contains `` without asking their check first. Repeatable. See <>. | `--json` | Emit the report as JSON instead of a step list — for a pipeline that wants to read it. | `--api ` | The admin node. You will not need this. | `--version`, `-v` | Print the version and exit. | `--help` | The same summary, from the command itself. |=== == The setup file [[the-setup-file]] `rhc setup` looks for `./rhc.setup.ts` in the working directory, or the path given by `--file`. [source,typescript] ---- import { defineSetup, step } from '@restheart-cloud/cli'; export default defineSetup('Notes app', [ step('notes collection', { check: ({ service }) => service.collectionExists('notes'), apply: ({ service }) => service.createCollection('notes'), }), step('authors may read their own notes', { check: ({ service }) => service.permissionExists('authorReadsOwnNotes'), apply: ({ service }) => service.putPermission('authorReadsOwnNotes', { predicate: "path-prefix('/notes') and method(GET)", roles: ['author'], priority: 10, }), }), ]); ---- The file imports the package, so the project needs its own copy alongside the global command: [source,bash] ---- npm i -D @restheart-cloud/cli ---- Two installs for two audiences: the global one gives you `rhc`, and outlives any single project; the local one is what the setup file's imports resolve against, since a global install is not on that path. === Every step is a check and an apply `check` answers "is this already so?" without changing anything. `apply` runs only when the answer was no, and the check runs again afterwards — an apply is never trusted to have worked. That shape is what makes a run safe to repeat. Against a service already configured, `rhc setup` writes nothing and reports every step satisfied. It is a deploy gate as much as a deploy step. `--dry-run` runs the checks and stops. It changes nothing and answers the question "what is this service missing". === Forcing a step [[forcing-a-step]] A check answers "does this exist?", and existing is not the same as being right. Edit a permission's predicate under the id it already had, or rewrite a schema under the same name, and the check still says yes — so the run reports the step satisfied and your change never reaches the service. `--force` names the steps to apply without asking first: [source,bash] ---- rhc setup --srv f3a9c1 --force 'consents gate' rhc setup --srv f3a9c1 --force catalog --force orders ---- The match is case-insensitive and on a substring, so a fragment of the step's name is enough. It skips the question, never the verification: the re-check still runs, and a forced step that did not work is reported failed like any other. CAUTION: Bare `--force`, with no name, takes every step — and that is usually the wrong tool. An apply written to run once may not survive running twice, and a step that seeds sample data will seed it again over whatever is there now. A check is partly what keeps those from happening. The honest fix, when a check keeps missing a change, is to deepen the check — compare the stored predicate, not merely its id. `--force` is what you reach for meanwhile. === Secrets are named, not held [source,typescript] ---- import { fromEnv } from '@restheart-cloud/cli'; 'secret-key': fromEnv('STRIPE_SECRET_KEY'), ---- `fromEnv` puts the *name* of a variable in the setup file, and resolves it at apply time. A stored secret reads back from the server as bullets, so a step that keeps what is already configured needs no secret in the environment at all — which is what lets a pipeline re-run a setup without every key it once needed. A missing variable fails by name, and names all of them at once rather than one per run. === What a step can reach `check` and `apply` receive `service` and `admin`. `service` speaks to your service — collections, indexes, permissions, users, schemas — authenticated with a token `rhc` mints for itself; you never handle one. `admin` speaks to RESTHeart Cloud — installing plugins, reading and writing their configuration, running their initialisers. == From a pipeline Set `RH_CLOUD_TOKEN` from your platform's secret store. There is no `rhc login` step: the variable **always wins over a stored session**, in that direction and with no condition attached, so a CI run can never quietly fall back to a session left behind on a shared runner. [source,yaml] ---- # .github/workflows/deploy.yml - run: npx @restheart-cloud/cli setup --srv f3a9c1 env: RH_CLOUD_TOKEN: ${{ secrets.RH_CLOUD_TOKEN }} STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }} ---- Never a flag: a credential in a flag is a credential in the shell history, and in the process list of every other user on the machine. === Exit codes [cols="1,4"] |=== | Code | Meaning | `0` | Every step satisfied or applied | `1` | A step failed | `2` | A dry run found work outstanding — configuration drift, not an error |=== `2` is what makes `--dry-run` useful as a check: a pipeline can fail on drift without applying anything. == Provisioning is deliberately not here `rhc setup` cannot create a service, and no setup file can. A setup is re-run on every merge, and a step able to create a service would create one per merge — a step able to create a *paid* one would start a purchase per merge. The `cli` role enforces this rather than the CLI merely declining: a token cannot start a Stripe Checkout session at all. See xref:tokens.adoc#it-carries-the-cli-role-not-yours[the `cli` role]. == Troubleshooting **`rhc` runs something about OpenShift, or fails with `bad interpreter`.** An unrelated `rhc` — Red Hat's OpenShift v2 client, a Ruby gem retired in 2017 — installs a script of the same name, usually in `/usr/local/bin`. Whichever comes first in `PATH` wins. `type -a rhc` shows both; remove the stale one, or reorder `PATH`. **`Cannot find package '@restheart-cloud/cli' imported from …`.** The setup file resolves its imports from its own directory, and a global install is not on that path. `npm i -D @restheart-cloud/cli` in the project the setup file belongs to. **A relative import in the setup file does not resolve.** Node loads it as a real ES module and does not guess extensions: write `./config.ts`, not `./config`. **`Unknown file extension ".ts"`.** Node 22.18 and later strip types on their own. Anything earlier wants `npx tsx`. == Related pages * xref:kit.adoc[The Cloud Kit] — the other half: the client code that talks to the service `rhc` configures * xref:tokens.adoc[Tokens] — the credential `rhc` authenticates with * https://github.com/SoftInstigate/restheart-cloud-starter-react[the starters] — each ships an `rhc.setup.ts` you can read