Repository navigation
Description
Problem
dirsql with no arguments prints a usage error pointing at dirsql server (packages/rust/src/cli/run.rs:296-301). It does not start the server — an earlier belief to the contrary was wrong — but an error is still the wrong default.
sqlite3 sets the expectation: run the binary bare, get an interactive shell. dirsql should do the same — a REPL accepting SQL against the current directory (path-tables, plus named tables when -c is passed).
Current dispatch
| invocation | behavior |
|---|---|
dirsql "<sql>" |
one-shot query (default mode, synonym dirsql query) |
dirsql server |
HTTP server |
dirsql init |
starter config |
dirsql (bare) |
usage error, exit 2 |
Slices
Stacked — each PR's base is the branch below it.
- REPL slice 1: core loop, TTY/pipe split, exit, banner #987 — slice 1: core loop, TTY/pipe split, exit, banner. Ships the complete non-TTY behavior plus a plain interactive loop.
- REPL slice 2: line editing, history, multi-line via reedline #988 — slice 2: line editing, history, multi-line via reedline. Interactive path only; the pipe path stays a plain reader.
- REPL slice 3: table output in TTY, JSON in pipes #989 — slice 3: table output in TTY, JSON in pipes.
Why this is an epic rather than one PR
Slice 3 is the reason. Slices 1–2 reuse the existing JSON array serialization, which is correct for dirsql query and wrong for a REPL — SELECT * FROM './' in a 5k-file repo dumps a 5000-element JSON array at a human. One-shot output is piped to jq; REPL output is read with eyes. Different renderings, and a real product decision rather than a detail of slice 1.
The feature is mostly assembled machinery
run_query (run.rs:308) is three calls: load_state (run.rs:434), report_scan_failures, and execute_query (cli/execute.rs:59). A REPL is a loop around the third. load_state and execute_query need no changes.
Two things fall out for free:
- Zero new dependencies for slice 1.
std::io::IsTerminalis std since 1.70; the workspace is on 1.85 / edition 2024. - Zero new flags. Bare
dirsqltakes no subcommand, soConfigArgsis already flattened onto the top-levelCli(run.rs:60-63) —-c,--persist,--no-ignore,--on-fileapply to the REPL with no parser work. (Confirmargs_conflicts_with_subcommandsdoes not bite with no SQL present — one parse test.)
Decisions made
- One
AppState, built once before the loop — not a loop of one-shot queries. Answers the original open question: it is cheaper (no directory re-scan per statement), a smaller diff, and the live watcher keeps the index fresh across statements. - No dot-commands. Answers the other open question. The dot prefix exists in
sqlite3to namespace meta-commands against SQL; with no meta-commands there is nothing to namespace. Exit is bareexit/quit/ Ctrl-D — unambiguous, since neither is a valid SQL statement. If meta-information is wanted later, it should be queryable as SQL (sqlite_master), which fits dirsql's premise better than a command namespace. - Exit code 0 on clean EOF, regardless of per-statement failures (matches interactive
sqlite3). - Static sample-query banner on TTY start — sample queries only, no live data rows and no scan (see Configless startup walks the entire tree for a provably empty result #986).
- Table in TTY, JSON in pipes (slice 3).
Open question — owned by #989
Once two output formats exist, can the user pick? .mode is the obvious answer and dot-commands are ruled out. Alternatives: infer-from-TTY-only (no control), or a --format {auto,table,json} flag valid in both modes. Decide before slice 3.
Behavior change
echo "SELECT 1" | dirsql goes from an exit-2 usage error to running the statement. Verified: nothing in the repo asserts the old behavior — only no_subcommand_and_no_sql_selects_neither (run.rs:667) touches it, and it asserts parsing, not the runtime error. Slice 1 carries the required migration fragment.
Testing
The TTY path is e2e-testable — via curtaincall (real PTY + VT100, pytest plugin with auto-waiting locators and snapshot support). Lands in packages/python/tests/e2e/ as e2e tier, since it drives the CLI with nothing mocked.
One caveat: curtaincall declares requires-python = ">=3.12" while dirsql's python package is >=3.10, so it needs an environment marker plus a skip guard. Filed upstream as thekevinscott/curtaincall#26 — its full suite (200 tests, including 74 real-PTY integration tests) passes unmodified on 3.10.20, so the floor looks like a policy default rather than a technical constraint. If that lands, the marker and guard are unnecessary.
Related
- Configless startup walks the entire tree for a provably empty result #986 — configless startup walks the whole tree for a provably empty result. Not a blocker, but a REPL that walks the tree before printing its first prompt is a bad first impression, and unlike a one-shot run the user is watching.
Activity
[-]Bare `dirsql` should open an interactive REPL[/-][+]Epic: bare `dirsql` opens an interactive REPL[/+]feat(cli): render rows as a table when they are read rather than piped
test: pin `--format` and the table rendering (RED)
thekevinbot commentedon Aug 21, 2026
All three slices landed: #987 via #991, #988 via #998, #989 via #1006 (format control resolved as --format {auto,table,json}). Bare dirsql now opens a REPL.