EnfinitOSEnfinitOS
DevelopersOperator & brand
Production-ready scaffold

EnfinitOS CLI

Operator-facing terminal client — rights, offers, challenges, proof packs, consent, drills, audit export, pilots.

@enfinitos/cliSubstrate ALLTypeScript
Install

Get the SDK

npm install -g @enfinitos/cli

About this status badge

Typed, tested, documented, and grounded in the 2026 platform reality. Awaiting first customer-integration validation.

README

The developer-facing documentation in full

Rendered from packages/sdks/cli/README.md at build time — the same source the package ships with.

@enfinitos/cli

EnfinitOS operator-facing terminal CLI — rights, offers, challenges, proof packs, consent, drills, audit export, and pilot programmes, all driven from a single binary.

The CLI is the operator-side counterpart to the platform's HTTP API. Where the operator-web package gives a reviewer a point-and-click console, this package gives an SRE, a compliance officer, or a backend pipeline a scriptable terminal command. The two packages share the same Transport + error hierarchy under the hood — the CLI just renders results to stdout/stderr instead of to React state.

Why a CLI

Operators reach for a CLI in three situations the web UI cannot serve as efficiently:

  1. Pipelines. enfinitos rights ls --status ACTIVE --format json | jq plugs straight into shell or CI scripts. No headless browser.
  2. Reproducible incident response. When something is wrong at 3am, you want a flat, copy-pasteable command history. The CLI prints the structured request, the exit code, and the response on every line.
  3. Bulk ops. Issuing 200 rights from a CSV is one shell loop away once you have an idempotent enfinitos rights issue that takes a --scope flag — much harder via a web form.
Auth shape. Today the CLI consumes a bearer JWT (set via enfinitos login --token … or ENFINITOS_API_KEY). The enfinitos login command will grow an OAuth 2.0 Device Authorisation Grant flow in a follow-up release; the shape is documented in src/commands/misc/login.ts so the eventual rollout is a drop-in change for operators.

Install

The CLI is a workspace-internal package today. From a fresh clone:

pnpm install
pnpm --filter @enfinitos/cli build
pnpm --filter @enfinitos/cli exec enfinitos --help

A standalone publish is on the roadmap (npx @enfinitos/cli …, plus Homebrew + apt). Once published, install with:

# globally
npm install -g @enfinitos/cli
enfinitos --help

# or ad-hoc
npx @enfinitos/cli rights ls

Configuration

Persistent settings live in ~/.enfinitos/config.json. The file is created on first enfinitos config set …; it stores at most:

{
  "version": 1,
  "apiUrl": "https://api.enfinitos.example.com",
  "apiKey": "ey…",
  "orgId": "org_…",
  "defaultFormat": "table",
  "refreshToken": "rt_…",
  "tokenExpiresAt": "2026-06-01T12:00:00.000Z"
}

Every value can be overridden by an environment variable for a single invocation (handy in CI):

SettingEnv varFlag
apiUrlENFINITOS_API_URL--api-url <url>
apiKeyENFINITOS_API_KEY(no flag — secrets)
orgIdENFINITOS_ORG--org <id>
defaultFormatENFINITOS_FORMAT--format <fmt>

Three precedence rules apply: command-line flag > env var > config file > built-in default. The CLI never writes secrets to logs. Token-shaped fields are partially masked (abcd…wxyz) by enfinitos config show.

Command surface

CommandWhat it does
enfinitos login [--token JWT]Establish a session. Device-flow OAuth coming soon.
enfinitos logoutClear local credentials.
enfinitos whoamiPrint the calling identity.
enfinitos config set <key> <value>Persist a config field.
enfinitos config showShow the merged config (secrets masked).
enfinitos version / enfinitos --versionPrint CLI + protocol version (version-info is a legacy alias).
enfinitos doctorDiagnose local env (network, file perms, version skew).
enfinitos rights ls [--status …]List rights with cursor pagination.
enfinitos rights get <rightId>Show a single right with provenance.
enfinitos rights issue --basis … --scope …Issue a root right from a verified basis.
enfinitos rights suspend <rightId> --reason …Suspend a right (reversible).
enfinitos rights revoke <rightId> --reason …Revoke a right (terminal).
enfinitos offers propose --right … --target …Propose an offer to another org.
enfinitos offers list [--status …]List offers (inbound/outbound/all).
enfinitos offers accept <offerId>Accept an offer (derives a new right).
enfinitos offers reject <offerId> --reason …Reject an offer.
enfinitos offers withdraw <offerId>Withdraw a proposed offer.
enfinitos offers counter <offerId> --terms …Counter-propose new terms.
enfinitos challenges open <rightId> --reason …Open a challenge against a right.
enfinitos challenges resolve <id> --outcome …Resolve a challenge (upheld\dismissed).
enfinitos challenges withdraw <id>Withdraw an open challenge.
enfinitos proof get <campaignId>Fetch the proof pack for a campaign.
enfinitos proof verify <pack.json>Locally verify a proof pack (offline).
enfinitos proof export <campaignId> --out …Export a campaign's proof pack to disk.
enfinitos consent issue --holder … --kind …Record a fresh consent grant.
enfinitos consent revoke <consentId>Revoke a consent record.
enfinitos consent check --holder … --kind …Probe whether a consent is currently active.
enfinitos drill run <gateId>Run a regulatory fire-drill scenario.
enfinitos drill listList all available drills.
enfinitos audit export --from … --to …Export the audit ledger over a date range.
enfinitos audit verify <audit.json>Verify a Merkle-included audit bundle.
enfinitos pilot statusShow pilot-programme enrolment.
enfinitos pilot enrol <orgId>Enrol an org in the pilot programme.

Every command supports --format table|json|yaml and --yes for unattended runs.

Output formats

The CLI picks a default format based on whether stdout is a TTY:

  • On a terminal: table (human-readable, ANSI-coloured when the terminal supports it; honours NO_COLOR and FORCE_COLOR).
  • Piped or redirected: json (so enfinitos rights ls | jq … works without flags).

Force a format with --format json, --format yaml, or --format table. Set ENFINITOS_FORMAT=json to make every CLI call in a session emit JSON without sprinkling flags.

The structured-error envelope is the same in all formats — a JSON object with ok: false, code, message, optional hint, and optional reasons. Table mode renders this prose-style; json/yaml modes emit it verbatim.

Exit codes

The CLI uses small, stable exit codes so shell pipelines can branch on them. They are exported from @enfinitos/cli's programmatic entry point as the EXIT constant:

CodeNameMeaning
0OKSuccess.
1GENERICUnclassified error.
2USAGEBad command-line invocation.
3AUTHAuthentication / authorisation failure.
4NETWORKCouldn't reach the platform.
5SERVERThe platform returned a 5xx envelope.
6VERIFICATIONA proof verify or audit verify failed.
7NOT_FOUNDThe target resource doesn't exist.
8CONFLICTThe operation conflicts with current state.

Architecture

+--------------------------+
|  bin/cli.ts (shebang)    |
+-----------+--------------+
            |
            v
+-----------+---------------------------+
|  runner.ts                            |
|    - commander tree (one cmd / file)  |
|    - context builder                  |
|    - error classifier + formatter     |
+-----+----------+-------------------+--+
      |          |                   |
      v          v                   v
+----------+ +----------+ +-----------------+
| commands/| | output.ts| |  transport.ts   |
|  rights  | |  - table | |  - HTTP client  |
|  offers  | |  - json  | |  - retries      |
|  proof   | |  - yaml  | |  - idempotency  |
|  consent | +----------+ |  - envelope     |
|  ...     |              |    parser       |
+----------+              +-----------------+
      |                          |
      v                          v
+---------------+        +-----------------+
| confirm.ts    |        |  config.ts      |
|  (enquirer)   |        |  (~/.enfinitos/ |
+---------------+        |    config.json) |
                         +-----------------+

Module boundaries

  • cli.ts is the bin entry — shebang, process.argv, process.exit. Nothing else.
  • runner.ts is the testable inside: it takes argv + env + streams + a transport factory and runs to completion. Tests exercise it end-to-end without touching the real network.
  • commands/<group>/<verb>.ts is one file per command. Each exports a handle<Group><Verb>(ctx, args) function — the runner is the only caller. The command file owns its input validation, HTTP call (via ctx.transport()), and output formatting.
  • transport.ts is a thin REST client with retry-with-backoff, idempotency-key generation, structured error mapping, and JSON envelope parsing. It does not depend on any single command.
  • config.ts owns ~/.enfinitos/config.json — JSON Schema-style validation, atomic writes, and the env-merge precedence rules.
  • output.ts is the table/json/yaml dispatcher plus the small ANSI-aware table renderer. Lives outside any command file so all groups render the same way.
  • errors.ts is the typed error hierarchy + the classify() function that turns any thrown value into a structured envelope plus an exit code.

Programmatic API

The CLI exports a small programmatic surface from @enfinitos/cli so other packages (notably operator-web) can re-use the same Transport, config loader, and proof-pack verifier without spawning a subprocess:

import {
  loadConfig,
  effectiveConfig,
  transportFromConfig,
  verifyProofPack,
  EXIT,
} from "@enfinitos/cli";

const config = effectiveConfig(await loadConfig(), process.env);
const transport = transportFromConfig(config);
const res = await transport.request({ method: "GET", path: "/v1/rights/123" });

const verification = verifyProofPack(JSON.parse(await fs.readFile("pack.json")));
if (!verification.ok) process.exit(EXIT.VERIFICATION);

The run() entry point is also exported, so embedding the CLI in a different binary is one function call:

import { run } from "@enfinitos/cli";
const code = await run({ argv: process.argv.slice(2), returnExitCode: true });
process.exit(code);

Getting started

# 1. point the CLI at your platform
enfinitos config set apiUrl https://api.enfinitos.example.com

# 2. authenticate (interactive device flow coming soon)
enfinitos login --token "$ENFINITOS_API_KEY"

# 3. sanity-check the local env
enfinitos doctor

# 4. real work
enfinitos rights ls --status ACTIVE --substrate DOOH
enfinitos rights get rgt_abc123
enfinitos rights issue --basis bas_xyz --scope ./scope.json

# 5. proof export + offline verification
enfinitos proof get cmp_2026q1 --out cmp_2026q1.pack.json
enfinitos proof verify cmp_2026q1.pack.json

Testing

Tests run under Vitest and live in src/__tests__/. The CLI is tested end-to-end at the runner boundary — every test passes argv + env + an injected fake Transport into run() and asserts on captured stdout/stderr.

pnpm --filter @enfinitos/cli test
pnpm --filter @enfinitos/cli typecheck

The fake Transport (__tests__/_fakeTransport.ts) supports scripted request/response sequences with per-call assertions on the outgoing path, method, body, and idempotency key. The capture helpers (__tests__/_capture.ts) snapshot stdout/stderr so each test asserts on the exact bytes the operator would have seen.

Example workflows

Bread-and-butter operator recipes — each one shells out to a single pipeline. All emit --format json so they slot into automated jobs.

Find all open challenges and resolve them

# 1) list every open challenge (cursor-paginated)
enfinitos challenges list --status open --format json \
  | jq -r '.data.items[].id' \
  | while read -r cid; do
      # 2) inspect, then resolve. Substitute upheld/dismissed as
      #    your judgement requires.
      enfinitos challenges resolve "$cid" --outcome dismissed --yes
    done

Export the last 30 days of audit

FROM=$(date -u -d '30 days ago' +%Y-%m-%d)
TO=$(date -u +%Y-%m-%d)
enfinitos audit export --from "$FROM" --to "$TO" --out audit.json
# Optional: verify the bundle locally before archiving.
enfinitos audit verify audit.json

Run all CRITICAL drills and write evidence

enfinitos drill list --format json \
  | jq -r '.data.items[] | select(.priority=="CRITICAL") | .gateId' \
  | while read -r gate; do
      enfinitos drill run "$gate" \
        --evidence-out "evidence/${gate}-$(date -u +%Y%m%dT%H%M%SZ).json" \
        --yes
    done

Bulk issue rights from a CSV

# columns: basis_id,scope_json_path
while IFS=, read -r basis scope; do
  enfinitos rights issue --basis "$basis" --scope "$scope" --format json \
    | jq -r '.data.id' >> issued-rights.txt
done < rights-to-issue.csv

Watch an inbox for new offers

while sleep 60; do
  enfinitos offers list --status open --direction inbound --format json \
    | jq -c '.data.items[] | {id, terms, target}'
done

Roadmap

  • OAuth 2.0 Device Authorisation Grant for enfinitos login (RFC 8628). Today the command accepts a token via --token; the fully-fledged flow lands in the next milestone.
  • Standalone publish. The bin/ entry is wired today; we still need release tooling to ship per-platform binaries (npm, Homebrew, Linux package archives).
  • enfinitos audit verify for differential bundles. Today the command verifies a self-contained bundle; the differential bundle format lands when the audit module's snapshot tooling does.
  • Tab completion. Commander supports completion generation for bash/zsh/fish; we ship the generator + an install hook.
API reference

Hit the HTTP surface directly

The EnfinitOS CLI is a thin client over the same governed HTTP API every other SDK calls. The full OpenAPI 3.1 reference lives on the docs site.

Sandbox

Run this SDK against a real tenant

The hosted sandbox is the fastest way to verify EnfinitOS CLI against a real EnfinitOS tenant before committing to a pilot. Launching Q4 2026.