Skip to content

feat: terminal CLI with interactive SQL shell#313

Draft
debba wants to merge 4 commits into
mainfrom
feat/terminal-cli
Draft

feat: terminal CLI with interactive SQL shell#313
debba wants to merge 4 commits into
mainfrom
feat/terminal-cli

Conversation

@debba

@debba debba commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

Tabularis now works from the terminal. The binary gains a set of clap subcommands that reuse the same connection resolution as the MCP server (keychain passwords, SSH/K8s tunnels, plugin drivers), so every saved connection is reachable from a script or a shell session, addressed by connection id or name. No Tauri runtime is started in this mode.

Command map

Command Description
tabularis connections (ls) List saved connections (table or --json)
tabularis databases <conn> List databases on the server
tabularis schemas <conn> List schemas
tabularis tables <conn> List tables (-d, --schema, --json)
tabularis describe <conn> <table> Columns, indexes, foreign keys
tabularis query <conn> [SQL] (q) One-shot query, stdin pipe, or interactive shell
tabularis install-cli Symlink the binary into a PATH directory

query picks its mode from the invocation: SQL argument → one-shot; piped stdin → executes the piped statement; interactive TTY with no SQL → opens the shell.

Output formats: aligned ASCII table (default), --format json (array of row objects), --format csv. Result data goes to stdout and logs to stderr, so piping stays clean. Non-zero exit code on failure.

$ tabularis query tabularis-demo-mysql "select id, name from customers" --format csv > customers.csv
$ echo "select count(*) from orders" | tabularis q tabularis-demo-mysql

Interactive shell

tabularis query <conn> opens a REPL backed by rustyline: line editing, persistent history (cli_history.txt in the app config dir), multi-line statements executed on a terminating ;, Ctrl-C drops the current buffer, Ctrl-D exits. Meta commands:

\l    list databases        \f table|json|csv   output format
\dn   list schemas          \limit N            row limit (0 = unlimited)
\dt   list tables           \schema NAME        set schema
\d T  describe table        \use DB             switch database
\q    quit                  \?                  help

Multi-database connections

Multi-db connections resolve to their first database, which made the remaining ones unreachable outside the GUI. All db-scoped commands now accept -d/--database, and the shell adds \use <db>: the switch is validated with a connection test before being applied, and the prompt shows the active database (Demo · MySQL:blog_demo>). This reuses the GUI's per-call database override semantics (DatabaseSelection::Single).

Internals

  • The connection-resolution helpers that lived in mcp/mod.rs (keychain lookup, SSH/K8s expansion, driver registration) moved to a shared headless.rs; the MCP server now wraps them with its JSON-RPC error type. No behavior change on the MCP side.
  • keychain_utils logged via println!, which polluted stdout (a problem once stdout carries parseable CSV/JSON) and bypassed the in-app log buffer. It now goes through the log crate.
  • Headless processes never called sqlx::any::install_default_drivers(), so the default test_connection path panicked. It is now installed in headless::register_drivers(), which also covers --mcp.
  • CLI argument parsing keeps the GUI-launch fallback: unknown arguments (macOS -psn_*) still boot the GUI, while misspelled subcommands and bad flag values surface clap's error instead of silently opening a window.

Tests

37 new unit tests: clap parsing (GUI fallback error kinds, value enums, aliases), table/CSV/JSON rendering (alignment, control-character escaping, quoting), limit and database-override semantics, and the install-cli symlink logic (idempotency, refusal to clobber foreign files, --force). Full suite: 692 passing.

Manually exercised against the demo MySQL connection: one-shot queries, stdin pipes, the shell with \use across the three demo databases, and install-cli into a scratch directory.

Known limitations

  • On Windows release builds the binary has no attached console (windows_subsystem = "windows"), so CLI output is invisible there. Same constraint as the existing --mcp mode.
  • Each shell statement runs on its own pooled connection: session state (SET, transactions, temp tables) does not persist between statements. Stated in \?.

Notes

This PR doubled as an experiment in agent-driven development: implementation, tests, and this description were produced end to end with Fable 5 (Anthropic), with human direction and review throughout.

debba added 4 commits June 9, 2026 22:19
- New clap subcommands: connections, databases, schemas, tables,
  describe, query, install-cli
- `query <conn>` without SQL opens an interactive shell (rustyline):
  multi-line statements, persistent history, psql-style meta commands
  (\dt, \d, \l, \dn, \use, \f, \limit, \schema)
- Multi-database connections: -d/--database on each command and \use
  in the shell, mirroring the GUI's per-call database override
- Extract shared headless connection resolution (keychain, SSH/K8s
  tunnels, driver registry) from the MCP server into headless.rs
- Route keychain logging through the log crate instead of raw stdout
- Install sqlx Any drivers in headless mode (test_connection panicked
  without them)
- install-cli symlinks the binary into /usr/local/bin or ~/.local/bin
Add a "Command Line" section to Settings > General to install, remove
and reinstall the 'tabularis' command without leaving the GUI,
mirroring VSCode's "Install 'code' command in PATH".

- Backend: get_cli_install_status / install_cli_shortcut /
  remove_cli_shortcut Tauri commands on top of cli::install, which now
  exposes structured state (installed, link path, in PATH, removable).
- Status detection recognizes both symlinks created by install-cli and
  the binary itself already reachable via PATH; removal only ever
  deletes a symlink that resolves to the running binary, never foreign
  entries or package-manager installs.
- The UI shows the installed path, a copyable PATH export hint when the
  bin dir is not in PATH, and offers force-replace only after a
  foreign-entry conflict.
- GUI management is gated to macOS, where the binary lives at a stable
  path inside the .app bundle: on Linux the binary is either already in
  PATH or at an ephemeral AppImage/Flatpak path, so the section is
  hidden there. The install-cli subcommand stays available on all Unix.
- settings.cli.* strings added to all 8 locales; unit tests for the new
  Rust helpers and the TS utils.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant