qqq is the command-line client and full-screen terminal UI for QQQ
applications. It talks to a QQQ server only through the UI API (/qqq/v1), the same API the web UI
uses, so apps, tables, fields, processes, widgets and extensions all come from the server's metadata and
one binary works against any QQQ application.
This repository also holds the TypeScript libraries qqq is built on, for anyone writing their own CLI,
TUI or native client. Current release: 1.0.0 (release notes,
CHANGELOG).
- Install
- The CLI in 60 seconds
- The terminal UI in 60 seconds
- Compatibility
- Packages
- Documentation
- Development
Every install path below except npm ships the same single-file binary with the CLI and the terminal UI
(qqq ui); no Node or Bun is needed. Builds exist for macOS (arm64, x64), Linux (x64, arm64; glibc, and
musl for Alpine) and Windows (x64).
macOS and Linux, curl installer (to ~/.local/bin; checks the download against the release's
SHA256SUMS). It installs the newest stable release, the same channel as Homebrew and Scoop;
--version pins one, --pre takes prereleases too, and --dry-run shows what it would download:
curl -fsSL https://raw.githubusercontent.com/QRun-IO/qqq-cli/main/scripts/install.sh | sh
curl -fsSL https://raw.githubusercontent.com/QRun-IO/qqq-cli/main/scripts/install.sh | sh -s -- --version 1.0.0 --dir /usr/local/binHomebrew (macOS and Linux, from QRun-IO/homebrew-tap;
installs bash, zsh and fish completions too):
brew install qrun-io/tap/qqqScoop (Windows, from QRun-IO/scoop-bucket):
scoop bucket add qrun https://github.com/QRun-IO/scoop-bucket
scoop install qqqDirect download: pick qqq-<os>-<arch>[-musl][.exe] from the
releases and check it against SHA256SUMS. On Alpine,
the musl build needs apk add libstdc++ libgcc.
npm, CLI only (GitHub Packages, Node.js 20 or later; no qqq ui, which says where to get it):
echo "@qrun-io:registry=https://npm.pkg.github.com" >> ~/.npmrc
npm install -g @qrun-io/qqq-cliGitHub Packages asks for a token even for public packages: npm login --registry=https://npm.pkg.github.com
with a GitHub token that has read:packages.
macOS, downloaded with a browser: the binaries are not notarized yet, so Gatekeeper stops a binary a
browser downloaded ("cannot be opened because Apple cannot check it for malicious software"). Clear the
download flag once with xattr -d com.apple.quarantine ./qqq-darwin-arm64 (or right-click, Open, in
Finder). Homebrew and the curl installer do not set the flag and are not affected.
Windows: the .exe is not Authenticode-signed yet, so SmartScreen may warn on first run (More info,
Run anyway). Scoop installs are not affected.
Check the install with qqq --version (it names the UI API range and whether the binary has the UI) and
qqq doctor.
The grammar is qqq <noun> <verb> [arguments] [options], with global options anywhere. The examples run
against the QQQ sample server on http://localhost:8077 (MOCK authentication, so no password); point
--server at your own server instead.
Sign in. OAuth2 and Auth0 servers use the device flow (or --browser for authorization code with
PKCE on a loopback redirect), TABLE_BASED servers ask for a username and password, MOCK and fully
anonymous servers get a server session. The first sign-in becomes the default profile.
$ qqq login --server http://localhost:8077 --profile sample
Signed in to http://localhost:8077 (mock authentication) as profile "sample"; sign-in kept in the macos-keychain.
$ qqq whoami
key value
------------ ------------------------------------------------------------
profile sample (default)
server http://localhost:8077
auth mock session
apiVersion 1.8.0
extensions esb 1.1.0
apps 6
...Query a table with a filter, then read one record:
$ qqq table query person -w "id <= 3" --fields id,firstName,lastName,email
Id First Name Last Name Email
-- ---------- --------- ---------------------
1 Avery Sample avery@example.invalid
2 Blair Sample blair@example.invalid
3 Casey Sample casey@example.invalid
(3 records; filter: id LESS_THAN_OR_EQUALS 3)
$ qqq table get person 1
Section Field Value
--------------- ------------- ---------------------
Identity Id 1
Identity First Name Avery
Basic Info Email avery@example.invalid
Employment Info Annual Salary $75,003.50
...
associations: pets (--associations lists the child records)Run a process (every step screen is supported; values come from -v, records from -r, and
anything missing is asked for):
$ qqq process run greetInteractive -r 1,2 -v greetingPrefix=Hi
Greet Interactive: starting
No Of People Greeted: 2
Id First Name Greeting Message
-- ---------- ----------------
1 Avery Hi Avery null
2 Blair Hi Blair null
(2 records)Export to CSV, XLSX, JSON or TSV:
$ qqq table export person -w "id <= 5" --fields id,firstName,email -o people.csv
wrote people.csv (206 bytes, text/csv;charset=utf-8)More of the grammar (-o/--out is always the output file; the sort is --order, prefix - for descending):
qqq table query person -g "(firstName = Avery OR firstName = Blair) AND birthDate > now(-40y)"
qqq table query person -w "lastName STARTS_WITH S" --order -id -l 5 # order by id descending, 5 rows
qqq table create pet --set name=Pixel --set personId=2 --set speciesId=Dog
qqq table bulk-insert person --file people.csv --dry-run # map, validate and review; --yes loads
qqq home # what you may open, with the command for each
qqq find person # the command palette as a command
qqq api getTableMetaData --path tableName=person -Q 'fields.*.label' # any of the 219 operations by id
eval "$(qqq completion zsh)" # also bash, fish and PowerShell| Noun | Verbs |
|---|---|
table |
list, describe, query, count, stats, get, create, copy, update, delete, export, children, download, variants, columns, actions, bulk-insert, bulk-edit, bulk-delete, apis, api-doc, api-call, developer, widget-form |
process |
list, describe, run (every step screen, async jobs with progress, Ctrl-C cancels on the server) |
home, find, apps, search, tail, whoami |
the landing page, the command palette, app homes (--widgets), global search, the live event stream, who you are |
report, saved-report, saved-view, share, bulk-profile, audit, widget, script, job, prefs, help-search |
reports and saved reports (pivots, schedules), saved views, sharing, saved bulk-load mappings, audit history, dashboard widgets as text, associated scripts, jobs, preferences, help |
api <operationId> |
any UI API operation by id: the escape hatch |
history |
everything you did in the CLI and the TUI on this profile, like a shell history (history rerun <n>, history clear, --since, --grep) |
print, printer |
print records (print person 1 2), table lists (print list person -w ...), text (print text), follow and cancel print jobs (print job list|status|cancel), printers and print agents (printer list|use|show|forget|devices); needs the printing extension |
extensions, and one noun per extension |
esb, collab, health, alerts, assistant, sql-workbench, ..., offered when the server has the extension installed |
login, logout, config, cache, completion, doctor, ui |
sign-in, profiles and settings, the metadata cache, shell completion, the environment check, the terminal UI |
Output is a table on a terminal and JSON in a pipe; -O json|table|text|csv|yaml (or QQQ_OUTPUT)
chooses, and -Q reshapes it with JMESPath, as aws --query does. Values are the server's labels and
display values; DATE_TIME values are shown in your zone (--utc for raw instants). Text filters (~,
STARTS_WITH, ENDS_WITH, quick search) ask for case-insensitive matching; --case-sensitive asks for
an exact-case match.
Profiles and sign-ins. Profiles name servers, as aws --profile does: qqq --profile prod ...,
QQQ_PROFILE, qqq profile list|add|edit|use|remove (each with a short name and a description; also
qqq config profile …), and Ctrl+O in qqq ui switches between them. Sign-ins live in the OS keyring (macOS Keychain,
Secret Service on Linux, DPAPI on Windows) or a 0600 file with QQQ_KEYRING=file for headless hosts.
OAuth2 tokens are refreshed before they expire; qqq logout --provider-logout also revokes them at the
provider. QQQ_TOKEN overrides everything with a bearer token, and QQQ_PASSWORD answers a
TABLE_BASED password prompt.
Exit codes: 0 ok, 1 error, 2 usage or bad filter, 3 auth, 4 server unreachable or rate limited,
5 not found, 6 validation, 70 server error, 130 interrupted. Every request carries
X-QQQ-Client: cli/qqq-cli <version> for the server's audit trail.
Printing. On a server with the printing extension (qbit-printing 2.x) and a print agent connected,
qqq printer list shows the printers (approved, exposed by their agent, formats), qqq printer devices
the agents and their health, and qqq print person 1 2 --printer "Virtual PDF printer" renders each
record as a record sheet, queues one job per record and follows them until they have printed (--no-wait
returns at once; qqq print job status <id> --watch follows later; qqq print job cancel <id> cancels a
job still queued). The printer given with --printer (or qqq printer use) is remembered per profile.
qqq print list person -w "lastName = Odd" --order -id --columns id,firstName,lastName prints a table list
of the whole result (up to 10,000 rows; --page 2 for one page), and some-command | qqq print text --title Log
prints text. Add --to-file [folder|file] to any of them to save the PDF instead of printing (no path: the
download folder, with the stamped name exports use). In qqq ui, Ctrl+P prints what you see on every
screen: a record sheet, a table list of the grid (all rows or this page), or a screen's content (or the
screen as drawn), each page with the app's header and footer; Save as PDF… in the printer picker saves
it like a download. Tables print only where the server enabled Print
for them (qbit-printing 2.1 registerAll enables every table). For local testing without hardware,
scripts/print-agent.sh runs qqq-print-agent's virtual file printer, which writes each job to ~/Downloads
(or QQQ_PRINT_OUT; --dir shows the folder).
TRY-IT.md walks through every command against the sample server.
qqq ui opens a classic full-screen terminal UI on the same profiles and sign-ins as the CLI:
qqq ui # the default profile; qqq --profile sample ui
qqq ui table query person -w "lastName STARTS_WITH S" # open a screen directly, in the CLI grammar
qqq ui --check # check the terminal and tmux setup, offer fixes
qqq ui --theme light # high-contrast (default), classic or light- Home is the landing screen: greeting and connection, counts, recently viewed records, quick actions, application cards, your work (jobs, alerts) and the instance's home widgets. F10, Reset comes back here.
- The function-key bar at the bottom is always right for the current screen: F1 Help, F2 Search,
F3 View, F4 Edit, F5 Refresh, F6 Filter, F7 New, F8 Delete, F9 Menu, F10 Quit. When the terminal takes
the F-keys, Esc then a digit (or Ctrl+digit) stands for F1 to F10.
Shift+F1…F10 are a second bank (Keys, Servers, SQL, Bulk, Metadata, Views, Copy, Export, History,
Assistant), shown as a second bar row from 30 lines (
:fkeys one|two|auto). - F2 is Search: records across tables (quick search), the main way into the data.
/(or., Ctrl+K) is the find-anything palette: any app, table, process, widget, report, record or command, searched as you type, fast on instances with thousands of tables. Ctrl+O switches servers, Ctrl+P prints what you see. - Ctrl+G (or
:ask) opens the Assistant panel: ask the server's assistant about the record or grid on screen (the assistant extension with a model provider; Ctrl+D drops the context).qqq assistant ask "<question>"is the same from the CLI. - F9 is the command center: every action of the current screen and of the application, searchable.
:opens a command line that speaks the CLI grammar (:table query person -w "id <= 3",:theme classic,:quit), with history (Up, Down, Ctrl+R) and Tab completion. Commands that are not screens run in-process and show their output.- Esc goes back, and the screen below keeps its cursor, filter and page. PgUp and PgDn page everywhere; Ctrl+F and Ctrl+B always work.
Grids page on the server, sort, filter with a guided builder or the grammar, quick search, show column
statistics and export; records open with their sections, child lists and next/previous; forms handle
contingent fields, uploads and validation; processes run as a wizard with progress and cancel. With the
printing extension, p (Print…) on a record or a grid selection opens a printer picker and the jobs are
followed in the status line (print 1/3 RECEIVED), with :printjobs for the recent ones. Keys, screens
and settings: packages/tui/README.md.
| Supported | |
|---|---|
| QQQ servers | UI API 1.6.0 up to, not including, 2.0 (QQQ 4.1.0-RC.15 or later). qqq --version prints the range; qqq doctor checks the server's version. |
| Extensions | Extension commands run when the server reports the extension in GET /qqq/v1/version; when it is missing, or older than the client needs, they exit 5 and say which. qqq extensions lists both sides. |
| Binaries | macOS arm64 and x64; Linux x64 and arm64, glibc and musl; Windows x64 (qqq ui needs Windows Terminal or another ConPTY terminal). The Windows and Intel macOS builds are cross-compiled and not yet exercised in CI (docs/ci-lanes.md). |
| npm packages | Node.js 20 or later. @qrun-io/qqq-ui-api-client needs only fetch and Web Crypto (no Node built-ins), so it runs on Bun and Deno too; @qrun-io/qqq-core and the CLI use Node built-ins (files, keyring). |
Versioning: qqq and @qrun-io/qqq-core follow semver from 1.0.0. @qrun-io/qqq-ui-api-client
follows the UI API version of the document it is generated from (1.8.0 here), and
@qrun-io/qqq-ui-api-conformance has its own version. All four are released together; see
CHANGELOG.md.
| Package | Version | What it is |
|---|---|---|
@qrun-io/qqq-cli |
1.0.0 | qqq, the CLI, and its program as a library (runCli) |
@qrun-io/qqq-core |
1.0.0 | the client kernel: sessions and keyring, metadata cache, filter grammar, forms, processes, records, render model, output formats. No terminal code |
@qrun-io/qqq-ui-api-client |
1.8.0 | a typed TypeScript client generated from the merged UI API document (core plus 14 extensions), with OAuth2 device-flow and PKCE sign-in, token refresh and the realtime (SSE) stream |
@qrun-io/qqq-ui-api-conformance |
1.6.0 | a read-only conformance suite for any UI API server, and qqq-dev-idp, a local OIDC provider for testing native clients |
packages/tui |
in the binaries | qqq ui, the full-screen terminal UI (OpenTUI on Bun), bundled into every qqq binary; not an npm package |
All npm packages are published to GitHub Packages under @qrun-io.
A minimal client on the generated API, against the sample server:
import { createUiApiClient, createSession, unwrap } from '@qrun-io/qqq-ui-api-client'
const baseUrl = 'http://localhost:8077'
const { sessionUUID } = await createSession(baseUrl) // MOCK; OAuth2 servers: deviceSignIn
const api = createUiApiClient({ baseUrl, sessionCookie: () => sessionUUID, clientChannel: 'tui/my-tui 1.0' })
const page = await unwrap(api.POST('/qqq/v1/table/{tableName}/query', {
params: { path: { tableName: 'person' } },
body: { filter: { criteria: [{ fieldName: 'id', operator: 'LESS_THAN_OR_EQUALS', values: [3] }], limit: 20 } },
}))
console.log(page.records?.map((r) => r.values.firstName)) // [ 'Avery', 'Blair', 'Casey' ]npm run mock serves the merged document with Prism on
http://127.0.0.1:4010, to build against without a server, and qqq-ui-api-conformance --server <url>
checks a server against the document.
| Document | What it covers |
|---|---|
| TRY-IT.md | every CLI command and the TUI against the local sample server |
| packages/cli/README.md | the CLI package: install, configuration, environment, embedding runCli |
| packages/tui/README.md | qqq ui: keys, screens, the : line, settings, terminals that take keys |
| packages/core/README.md | the client kernel's modules |
| packages/ui-api-client/README.md | the typed client: calls, sign-in, errors, realtime |
| packages/conformance/README.md | the conformance suite and qqq-dev-idp |
| docs/README.md | index of everything under docs/ |
| docs/extensions.md | the extension documents merged into the client |
| docs/parity/feature-matrix.md | CLI and TUI status of every row of the web UI's acceptance matrix |
| docs/server-findings.md | UI API issues found while building the clients |
| docs/release-runbook.md | how a release is cut, signed, published and packaged |
| docs/packaging/README.md | the curl installer, Homebrew formula, Scoop manifest and platform notes |
| docs/ci-lanes.md | what CI runs: Linux verify and publish, macOS arm64 and Windows x64 lanes |
| CHANGELOG.md | every change by version |
The UI API itself is documented in QQQ:
UI-API-CLIENTS.md and
UI-API-CHANGELOG.md.
Node 20+ (CI uses 22) and npm workspaces for the four packages; Bun 1.4+ for the TUI and the binaries.
npm ci
npm run build && npm test && npm run lint
npm run test:tui # the TUI's tests (Bun)
node packages/cli/dist/main.js --help # the CLI from the checkout; `... ui` runs the TUI from source with Bun
npm run generate # regenerate the client and the operations index from the document
npm run spec:merge-extensions # remerge the extension documents (sample server running)
npm run binaries # dist/qqq-<os>-<arch>[.exe] with the UI, and SHA256SUMS; --host, --musl, --cli-only
npm run release # every release check, then the tag and publish commands (prints only)
scripts/verify-binary.sh dist/qqq-darwin-arm64 --server http://localhost:8077 # smoke a binaryThe CLI runs on Node 20 and later and uses no Bun-only API; Bun is needed only for the TUI and the
binaries. CI is the qqq-orb cli profile (.qqq-ci.yml); a tag v<client version> publishes the
packages to GitHub Packages (docs/ci-lanes.md says what it does and does not cover).
- CONTRIBUTING.md: setup, checks, commit and pull request rules (signed commits on
main). - SECURITY.md: report vulnerabilities privately, never in a public issue.
- CODE_OF_CONDUCT.md.
- Bugs and feature requests: issues.
Licensed under the Apache License 2.0. QQQ itself is a separate project with its own license.