@revos/cli
v0.8.2
Published
RevOS CLI for managing RevOS platform resources
Readme
RevOS CLI
Command-line interface for managing RevOS resources.
Installation
pnpm add @revos/cliPlatform support
Linux, macOS, and Windows. On POSIX systems the credentials store
(~/.revos/credentials.json) is created with 0600 permissions; on Windows
those bits are not applied (Windows uses ACLs). Treat your home directory's
.revos folder as sensitive on Windows and rely on standard user-profile ACLs.
Usage
revos [command] [options]Global Options
-o, --org <id> Override organization ID. Only accepted OUTSIDE a project —
inside a project, the org is anchored in revos.yaml and
--org is rejected.
--json Format output as JSONProject context and the org
The CLI walks up from the current directory looking for revos.yaml on every command. When it finds one:
revos apply / pull / diff / statusand every resource command (connections,cubes,tables, …) targetmetadata.orgIdfromrevos.yaml. This is authoritative.--organdREVOS_ORG_IDare validated againstrevos.yaml. A mismatch is a hard error — this prevents a Dev Container generated for org A from silently writing to org B when you switch contexts.- Your stored global default (
revos org switch) is informational only inside a project. The CLI warns if it differs from the project's org but uses the project's org for actual API calls.
Outside a project, the CLI uses REVOS_ORG_ID (env), then --org, then your stored global default (revos org switch). revos init, revos auth *, and revos org * skip the strict in-project enforcement so you can still log in, list, or switch orgs from inside a project tree.
Commands
Project Initialization
revos init [destination] [--yes] [--dry-run] [--no-pull]Scaffolds a new RevOS data engineering project. If destination is omitted, initializes in the current directory.
Arguments:
destination Path or project name (default: current directory)
Examples:
my-project → creates ./my-project/
./my-project → same as above
../my-project → one level up
/home/user/my-project → absolute pathFlags:
-y, --yes Skip confirmation prompts, use defaults
--dry-run Show what would be created without creating files or GCP resources
--no-pull Skip discovering existing remote Connections and CubesNon-empty directory behavior:
If the destination already exists and is not empty, you will be prompted to confirm before proceeding. Use --yes to skip this prompt.
What it does (greenfield — no revos.yaml discovered):
- Fetches your organizations and prompts you to select one (auto-selects if only one).
- Provisions a GCP service account for your org (idempotent) and writes the key to
~/.revos/{project-name}-gsa-creds.json. - Writes
revos.yaml— the project marker that pins this directory to your organization. CLI commands likerevos statuswalk up from the current directory to find this file. - Creates the project directory structure (medallion layout: bronze/silver/gold).
- Generates
.devcontainer/(devcontainer.json,Dockerfile,post-create.sh,post-start.sh,welcome.sh) with Python, Node.js, Google Cloud SDK (bq), GitHub CLI (gh),jq, and dbt pre-installed. The Dev Container persists three per-project named Docker volumes —revos-{slug}-credentials(~/.revos),revos-{slug}-gcloud(~/.config/gcloud), andrevos-{slug}-claude(~/.claude) — so CLI login, GSA key, gcloud auth state, and Claude Code state all surviveRebuild Container. No host bind mounts. - Generates
dbt/profiles.ymlpre-configured for BigQuery. - Generates
.gitignoreandREADME.md. - Scaffolds AI companion files:
CLAUDE.md,AGENTS.md, and.claude/settings.jsonwith deny rules that block AI assistants from reading~/.revos/(CLI credentials and service account keys),~/.config/gcloud/(Google Cloud Application Default Credentials), from runninggcloud auth print-*-tokencommands, and from editing the settings file itself (so the AI can't lift its own restrictions). - Installs the seven data-engineering skills into
.claude/skills/—explore-lakehouse,create-connections,create-cubes,create-dbt-transformations,load-sample-data,query-semantic-model, andvisualize-semantic-model— by runningnpx skills add revosai/skills/data-engineering --copy(the openskillsCLI). Skills in therevosai/skillssource-of-truth repo are grouped into bundles (one directory each); pointing at thedata-engineeringbundle installs exactly its skills, so as the repo grows bundles for other project types a data-engineering project still only gets the relevant ones. This step needs network access; if it fails, init still completes and prints the command to install them later. See Updating skills below for how they stay current and how to use them with other agents. - Discovers every Connection and Cube currently in the org and writes them under
connections/andcubes/so a developer joining an org with existing pipelines and cubes starts from the live state. Pass--no-pullto skip. Sources are managed only through the API surface (see Sources) and are referenced from Connection YAML by their server id.
What it does (in-project — re-run inside an existing revos.yaml tree):
revos init becomes idempotent: it skips the org prompt, skips scaffolding (no overwriting of existing files), skips IaC discovery, and only provisions the GCP service account key if ~/.revos/{project-name}-gsa-creds.json is missing. If you aren't authenticated yet, it opens the browser to sign in first — the org is taken from revos.yaml, so no picker is shown. Useful inside the Dev Container, where the named credentials volume starts empty: a single revos init both signs you in and materializes the GSA key. Outside the container, re-running is a safe no-op if everything is already in place.
Dev Container onboarding flow: open project in VS Code → "Reopen in Container" → run revos init. The first run signs you in via the browser and provisions the GSA key. Subsequent container starts auto-activate the service account via postStartCommand.
Requires: revos auth login first (greenfield, since the multi-org picker and --dev selection live there). In-project init handles login itself.
Updating skills
The skills under .claude/skills/ come from the public revosai/skills repository — the source of truth — and are installed (not bundled in this CLI) so they can be refreshed independently of the CLI version.
npx skills update # refresh .claude/skills/ to the latest from revosai/skillsThe generated Dev Container re-installs/refreshes the skills on every container start (best-effort, in post-start.sh) — it installs the bundle if a prior attempt failed (e.g. no network during init) and updates it to the latest otherwise. Projects opened in the container stay current automatically; Claude Code picks up the new files the next time it loads skills, with no plugin reload. Outside the container, run the command above when you want the latest.
Other agents. The skills aren't Claude-specific. Install them into any supported agent's directory:
npx skills add revosai/skills/data-engineering -a cursor --copy # e.g. Cursor
npx skills add revosai/skills/data-engineering -a claude-code cursor --copy # several at once
npx skills add revosai/skills/data-engineering --list # list the bundle's skills--copy writes real files rather than symlinks — committable and portable across machines, containers, and OSes.
Apply local resources
revos apply [path] [--project <path>] [--dry-run] [--parallelism <n>] [--json]Reconciles local revos resources with the API:
- Resources without
metadata.idare created (POST) and the YAML file is rewritten in place to record the returned id. - Resources with
metadata.idare read from the API and compared field by field; if the local spec drifts, the resource is updated (PATCH). - Resources that already match the API are reported as
unchanged.
Resources are applied in dependency order when one local resource references another. Within a level, up to --parallelism resources are processed concurrently (default 4). Unresolved references and cycles fail before any API call.
Two kinds are supported today: Connection (a sync pipeline from a Source into your org's data warehouse) and Cube (a Cube.dev semantic model definition). Connections reference a Source by its server id under spec.source.id. Get the id from revos sources list --json:
apiVersion: revos/v1
kind: Connection
metadata:
name: analytics-pipeline
spec:
name: "Analytics pipeline"
source: { id: src_abc123 } # from `revos sources list`
schedule: { units: 24, timeUnit: hours }
status: active
prefix: analytics_ # optional; auto-generated from the source if omitted
streams:
- name: users
namespace: public
syncMode: incremental_deduped_history
cursorField: [updated_at]
primaryKey: [[id]]
- name: orders
namespace: public
syncMode: full_refresh_overwriteSources themselves are not IaC — create them via revos sources create (opens the UI) and pull the id with revos sources list.
spec.prefix is prepended to every destination table name. It must match ^[a-z0-9_]*$ (lowercase letters, digits, and underscores) and be at most 63 characters. If omitted on first apply, the API auto-generates a prefix from the source (e.g. postgres_) and revos apply writes it back into your YAML so subsequent diffs are clean. Edit spec.prefix and run revos apply to rename it on an existing connection.
Data masking & stream mappers on a Connection
Each stream may carry a mappers: array — server-side transformations applied before rows land in BigQuery. Use them to hash PII, drop columns, rename fields, or filter rows out of the sync. Five types are supported: hashing, field-renaming, field-filtering, row-filtering, and encryption. The kind-specific configuration always lives under mapperConfiguration:
streams:
- name: customers
namespace: public
syncMode: incremental_deduped_history
cursorField: [updated_at]
primaryKey: [[id]]
mappers:
# Hash PII so analysts get joinable but irreversible identifiers.
- type: hashing
mapperConfiguration:
targetField: email
method: SHA-256
fieldNameSuffix: _hashed
# Rename a column on the way in.
- type: field-renaming
mapperConfiguration:
originalFieldName: ssn
newFieldName: ssn_redacted
# Drop a column entirely.
- type: field-filtering
mapperConfiguration:
targetField: internal_notes
# Keep rows where the predicate is true; the rest are dropped.
# NOT(is_test == "true") is true for every non-test row, so test
# rows get dropped. (Compare against a real sentinel value — empty
# strings behave unreliably.)
- type: row-filtering
mapperConfiguration:
conditions:
type: NOT
conditions:
- type: EQUAL
fieldName: is_test
comparisonValue: "true"
# Reversible encryption (RSA or AES). Keys are required.
- type: encryption
mapperConfiguration:
algorithm: AES
targetField: card_number
fieldNameSuffix: _enc
key: ${env.AES_KEY}
mode: GCM
padding: NoPaddingHashing methods: MD2, MD5, SHA-1, SHA-224, SHA-256, SHA-384, SHA-512. AES modes: CBC, CFB, OFB, CTR, GCM, ECB. AES paddings: NoPadding, PKCS5Padding. Masking is enforced at ingest — even a direct BigQuery query sees only the masked values.
A Cube carries the full Cube.dev semantic model directly under spec. spec.name is the cube identifier — snake_case (revenue_metrics), the name referenced inside ${CUBE}, cross-cube joins, and queries (Orders.count); it must be a valid SQL/JS identifier (the compiler rejects hyphens). metadata.name is the local slug — kebab-case (revenue-metrics) by convention, like every other resource. It's the filename and IaC address, never sent to the API, so renaming it (and the file) is a local-only move while editing spec.name renames the cube upstream. Every other Cube.dev field — sql_table, dimensions, measures, joins, meta — sits flat under spec:
apiVersion: revos/v1
kind: Cube
metadata:
name: revenue-metrics # local slug (kebab-case)
spec:
name: revenue_metrics # the cube identifier — snake_case (${CUBE}, joins, queries)
sql_table: "`my_project.my_dataset.gold_revenue`"
dimensions:
id:
sql: "${CUBE}.id"
type: string
primary_key: true
measures:
count:
type: countMigrating from legacy auto-generated cubes
If your org previously relied on auto-generated cube definitions, you can migrate using Claude Code (or any AI assistant with revos api access):
"Fetch
GET /cubes/previewviarevos api, convert each cube into an IaC YAML file undercubes/, then runrevos apply."
The AI will call the endpoint, transform the JSON response into the correct apiVersion: revos/v1 / kind: Cube YAML format, write the files, and push them.
Once revos apply succeeds, the org switches to reading cubes directly from the database — auto-generation is bypassed.
--dry-run reports what would change without writing to the API or YAML files. It still reads remote state to detect drift, so authentication is required.
The CLI walks up from the current working directory to find revos.yaml. Resources are addressed by kind/metadata.name; filenames are storage only.
Updates are last-writer-wins. Run
revos difffirst if you want to inspect drift before reconciling.
Diff local vs API
revos diff [path] [--project <path>] [--parallelism <n>] [--json]Shows the drift between local YAML and the API without making any changes. For each resource that would be created or updated, prints the changed fields:
update Connection/analytics-pipeline
~ schedule.units: 24 → 6
+ prefix: analytics_Sensitive fields (passwords, tokens, API keys, client secrets) are redacted as (sensitive value) so secrets never reach stdout. The same diff data is available via --json.
Pull from API
revos pull [path] [--project <path>] [--dry-run] [--force] [--json]The reverse of apply. For each registered kind, lists every remote resource:
- Known resources (matched against local YAML by
metadata.id) get theirspec:block rewritten in place when drifted, or markedunchangedwhen the live state already matches local. Sibling documents in multi-doc files, comments, andmetadataare preserved byte-for-byte; onlyspec:is rewritten. - Unknown resources are discovered — a fresh YAML file is written under
<kind-lowercase>s/<slug>.yaml. The slug comes from the server-sidenamefield, kebab-cased; collisions against existing local addresses dedupe with-2,-3, etc. - Local resources whose
metadata.iddoesn't exist on the server are reported asskipped (not found). - Local resources without
metadata.idare reported asskipped (no id)— they don't yet exist remotely; runrevos applyto create them.
Discovered Connections carry the server source id in spec.source.id, so the YAML you get out is immediately apply-able.
Sensitive fields (passwords, secrets, tokens) are dropped from discovered resources, since the server returns redacted placeholders that would corrupt the upstream service on a subsequent apply. For existing resources, the local file's value wins on pull — password: ${env.PG_PASSWORD} survives a round-trip without being baked in as the resolved value.
Refuses to overwrite local files with uncommitted git changes; pass --force to override. Newly-discovered files are always written (they can't conflict with anything on disk). --dry-run reports what would be pulled or discovered without writing.
Project Status
revos status [path] [--project <path>] [--columns address,state,id,source] [--json]Lists local revos resources (Connections, Cubes) discovered under the project root, with their state.
States:
pending— defined locally, not yet applied to the APIok— applied and synced to the APItampered—metadata.idlooks malformed; recover by runningrevos pull <kind>/<name>(lands in a follow-up phase)drifted— local and remote diverge; runrevos diffto inspect orrevos applyto reconcile
The CLI walks up from the current working directory to find revos.yaml (or set --project <path> / REVOS_PROJECT to override). Resources are addressed by kind/metadata.name; filenames are storage only.
Authentication
Login
revos auth loginAfter the OAuth flow completes, the CLI picks your default organization:
- Inside a project (
revos.yamldiscovered) — usesmetadata.orgIdfromrevos.yaml; no prompt. - Outside a project, one org — auto-selects it.
- Outside a project, multiple orgs — prompts you to pick.
Remote / headless machines (SSH, Codespaces):
By default login starts a temporary http://localhost callback server and opens
your browser. In GitHub Codespaces the CLI detects this automatically and
switches to a no-server flow: it prints a URL to open in any browser, and you
paste the code shown there back into the terminal.
For any other setup where the browser can't reach the CLI's localhost — an
SSH session, a plain container, a headless host — pass --no-localhost (or set
REVOS_AUTH_NO_LOCALHOST=1) to use the same paste flow.
revos auth login --no-localhostA local VS Code dev container needs neither — the editor forwards the callback port to your host and opens your host browser, so the default flow just works.
Logout
revos auth logoutShow auth status
revos auth status [--json]Organization Management
List organizations
revos org list [--columns id,name] [--json]Show current organization
revos org current [--json]Switch organization
# Interactive mode
revos org switch
# Direct by ID
revos org switch <org-id> [--json]--json requires an explicit <org-id> (interactive selection needs a TTY).
Sources
Manage data sources. list, get, and delete hit the API directly. create and update open the RevOS UI, since source configuration (connector picker, dynamic config schemas, OAuth flows) is not practical to drive from a CLI.
List sources
revos sources list [--json]Get source
revos sources get <id> [--json]List streams the source exposes
revos sources list-streams <id> [--ignore-cache] [--columns a,b,c] [--json]Discovers the streams (tables, endpoints, etc.) the source advertises and the per-stream metadata you need to fill in a Connection: supported sync modes, default cursor field, source-defined primary key, and the list of fields available for selection. Pass --ignore-cache to bypass the cached catalog and re-discover.
revos sources list-streams src_abc123 --json | jq '.[] | {streamName, syncModes, defaultCursorField}'Create source
revos sources createOpens the RevOS UI to add a new data source.
Update source
revos sources update <id>Opens the RevOS UI to edit an existing source.
Delete source
revos sources delete <id>Connectors
Author custom low-code connectors as code, versioned alongside your
connections/ and cubes/. A connector that isn't in the built-in source
catalog can be written locally, tested, and registered with revos apply —
after which it behaves like any other source type: create a Source against it
(with real credentials) and reference that Source from a Connection.
Each connector lives in its own directory under connectors/:
connectors/
my-source/
connector.yaml # RevOS resource wrapper (kind: Connector)
manifest.yaml # the low-code connector manifest
secrets/
config.json # the connector's config values (gitignored)
integration_tests/
configured_catalog.json# connectors/my-source/connector.yaml
apiVersion: revos/v1
kind: Connector
metadata:
name: my-source # local slug identity (folder + address); id is written back on apply
spec:
name: My Source # the registered name (shown in the UI); free-form
type: declarativeTwo names, two jobs (the same split a Connection uses):
metadata.nameis the local slug identity — the directory name and the address other resources reference it by. It is never sent to the API. Rename it (and the folder) for a local-only move;metadata.idkeeps the rename from orphaning, and the engine re-matches by that id, so nothing changes upstream.spec.nameis the connector's registered name (what the UI shows). Edit it and re-apply to rename the connector upstream — an ordinary spec change.
This keeps pull → diff clean even for free-form names: pull writes the
server name verbatim into spec.name, so a name like My Source no longer
drifts against the my-source slug. The manifest itself lives in the sibling
manifest.yaml — it is not duplicated in connector.yaml.
Commands
| Command | Behavior |
| --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| revos connectors new <name> | Scaffold a runnable hello-world connector (against a public API). |
| revos connectors spec <name> | Show the connector's config schema, read from its manifest. Pure-local. |
| revos connectors check <name> [--config …] | Run the connector locally and test the connection. Reads secrets/config.json (or --config). |
| revos connectors discover <name> [--config …] | Run the connector locally and list the streams it exposes. |
| revos connectors read <name> [--config …] [--catalog …] [--state …] | Run the connector locally and stream sample records. |
| revos apply [connectors/<name>] | Register (or update) the manifest(s) with RevOS — the whole project, or one connector by path. Idempotent; bumps the version when the manifest changes; writes the id back. The payload contains no credentials. |
| revos connectors delete <name> [--purge] [--yes] | Unpublish the connector from the workspace (by its recorded id) and clear that id locally, keeping the files. --purge also deletes the local directory; --yes skips the confirmation prompt. |
check / discover / read require the connector test runtime on PATH
(pre-installed in the RevOS Dev Container). new, spec, apply, and delete
do not. Outside the Dev Container, if the runtime is missing these commands stop
with the exact one-line install command to run — install it once and they work.
Credentials stay in secrets/, never committed
A connector's config values for the local test loop live in
secrets/config.json:
{ "api_key": "your-key-here" }secrets/ is gitignored, so real credentials you put there are never committed.
check/discover/read read this file (override with --config <path>, e.g.
CI secrets); spec needs none. The connector runs entirely on your machine —
nothing is uploaded. Real per-tenant credentials are entered later, server-side,
exactly as for built-in sources — never uploaded by revos apply.
The scaffolded .gitignore excludes connectors/**/secrets/,
connectors/**/config.json, connectors/**/*.env, and connectors/**/.env*.
Resource commands
The CLI exposes every method of @revos/api-client as a topic-prefixed command. Each topic mirrors a resource on the SDK and supports the standard CRUD verbs (list, get, create, update, delete) plus any resource-specific extras.
Topics
| Topic | Verbs |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| tables | list, get, create, update, delete |
| table-views | list, create, update, delete |
| cubes | list, get, create, update, delete, query, meta |
| connections | list, get, create, update, delete |
| scores | list, create, update, delete |
| score-groups | list, get, create, update, delete |
| gservice-accounts | list, get, create, delete |
| gservice-account-keys | get, reveal |
| actions | list, get, get-params-schema, get-input-schema |
| action-runs | list, get |
| ai-instructions | list, get, create, update, delete |
| org | list, current, switch, get, create |
| segments | list, get, create, update, delete, list-versions, get-version, restore-version, get-evaluation-history, evaluate |
List flags
list commands accept --page-size, --page-token, --order-by, --filter, --fields, and --columns. actions list adds --only-used.
revos tables list --page-size 50 --order-by 'createdAt desc'Each list command ships a curated default column set; use --columns to override (comma-separated). Pass --json for the full payload.
revos tables list # default columns
revos tables list --columns id,name # only id and name
revos tables list --json # full SDK responseRequest bodies (--body)
create and update take a single --body flag. Three forms:
# Inline JSON literal
revos tables create --body '{"name":"customers","sql_table":"customers"}'
# Read from file
revos tables create --body @./table.json
# Read from stdin
cat table.json | revos tables create --body -Bad JSON is rejected before any HTTP call.
Examples
revos tables list --json
revos tables get tbl_123
revos segments evaluate seg_456
revos segments list-versions seg_456
revos segments get-version seg_456 3
revos actions get-params-schema act_789
revos gservice-account-keys reveal key_abcQuery the semantic model (cubes query / cubes meta)
revos cubes meta lists every cube and view exposed by the org's semantic
model along with its measures, dimensions, and segments — use it to discover
the member names you can reference in a query.
revos cubes query runs a Cube.js query and prints the resulting rows.
--query accepts inline JSON, @path/to/file.json, or - to read from
stdin. The payload may either be a bare query object or wrapped as
{ "query": { ... } }. Rows render as a table by default; pass --json
for the full response payload.
revos cubes meta
revos cubes meta --json | jq '.cubes[0]'
# inline
revos cubes query --query '{"measures":["Orders.count"]}'
# file
revos cubes query --query @./query.json --json
# stdin
echo '{"measures":["Orders.count"],"dimensions":["Orders.status"]}' | revos cubes queryRaw API access (revos api)
Call any RevOS REST endpoint directly, reusing your stored auth, API base URL, and organization context. Useful for scripts, debugging, and endpoints not yet covered by typed resource commands.
revos api <path> [-X <method>] [-H 'Name: value']... [-q 'key=value']... [--body JSON|@file|-] [--json]| Flag | Meaning |
| -------------- | ------------------------------------------------------------------------- |
| -X, --method | HTTP method (default GET; case-insensitive). |
| -H, --header | Extra request header ('Name: value'). Repeatable. |
| -q, --query | Query parameter ('key=value'). Repeatable; repeated keys become arrays. |
| --body | Request body — inline JSON, @file, or - (stdin). |
| --json | No-op for content (output is already JSON); changes error formatting. |
Output is the pretty-printed response body, including the { data, metadata } envelope that list endpoints use — so metadata.nextPageToken stays visible for scripted pagination.
revos api /organizations
revos api /tables -q pageSize=50 -q 'orderBy=createdAt desc'
revos api /connections -X POST --body '{"name":"my-conn"}'
revos api /connections -X POST --body @body.json
revos api /segments/seg_123 -X DELETE
revos api /organizations -H 'X-Foo: bar'Errors follow the same conventions as other commands: 401 prompts you to run revos auth login, 404 reports the missing URL, 5xx surfaces the server error.
Output Formats
Every command supports --json for machine-readable output:
revos org list --json
revos auth status --json
revos status --jsonList commands ship a curated default column set, and accept --columns to override:
revos tables list --columns id,name
revos org list --columns name
revos status --columns address,state,idConfiguration
| Variable | Description |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| REVOS_TOKEN | Authentication token (alternative to revos auth login) |
| REVOS_API_URL | API endpoint (default: https://api.revos.ai) |
| REVOS_ORG_ID | Organization ID override. Inside a project, must match metadata.orgId in revos.yaml; mismatch is a hard error. |
| REVOS_AUTH_NO_LOCALHOST | Set to 1 to force the paste-a-code login flow (no local callback server). Auto-detected in GitHub Codespaces. |
| REVOS_TELEMETRY_DISABLED | Set to 1 to suppress all product telemetry. |
| DO_NOT_TRACK | Cross-vendor convention (consoledonottrack.com). Set to 1 to suppress all product telemetry. |
| REVOS_TELEMETRY_DEBUG | Set to 1 to print telemetry envelopes to stderr instead of sending them. |
