Skip to content

REPL slice 1: core loop, TTY/pipe split, exit, banner #987

Closed

Description

@thekevinbot

Slice 1 of 3 under #953.

Ships the complete non-TTY behavior and a working (if plain) interactive loop. No line editing yet — that is #953's slice 2.

Scope

  1. New packages/rust/src/cli/repl.rs, roughly:
    async fn run_repl<R: BufRead, W: Write>(
        state: &AppState, input: R, out: &mut W, interactive: bool
    ) -> u8
    Prompt (interactive only) → read line → skip blanks → execute_query → print result, or print the error to stderr and continue. This continue-on-error is the one real behavioral difference from run_query, which exits 1.
  2. run_default's None arm (packages/rust/src/cli/run.rs:296-301) swaps the usage eprintln! for load_state + run_repl.
  3. TTY/pipe split via std::io::stdin().is_terminal() — std since 1.70, workspace is on 1.85. TTY → prompt + banner; non-TTY → no prompt, same loop. One boolean covers both halves of Epic: bare dirsql opens an interactive REPL #953.
  4. Exit: bare exit / quit / Ctrl-D. No dot-commands — see Epic: bare dirsql opens an interactive REPL #953 for the rationale.
  5. Static sample-query banner on TTY start. Static text only — no live data rows, no scan (see Configless startup walks the entire tree for a provably empty result #986).

Reuses, unchanged

load_state (run.rs:434) and execute_query (cli/execute.rs:59) need no changes. Output is the existing JSON array serialization; the TTY table is slice 3.

Decisions already made (see #953)

  • One AppState, built once before the loop — not a loop of one-shot queries. Cheaper (no re-scan per statement), smaller diff, and the live watcher keeps the index fresh across statements.
  • Exit code 0 on clean EOF, regardless of per-statement failures (matches interactive sqlite3). Document it.
  • PARTIAL_SCAN_EXIT (23) has no REPL meaning — report skipped files once up front, drop the code.
  • AppState::Unavailable (bad config) fails identically on every statement — check readiness once before the loop and exit 1 there, rather than looping the same error forever.
  • Read stdin via tokio::task::spawn_blocking — run_cli wraps everything in runtime.block_on (run.rs:270), so a bare blocking read wedges a runtime worker.

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.

Testing

  • Unit: colocated inline #[cfg(test)] in repl.rs. The BufRead/Write DI signature exists precisely so unit lint's isolation rule is satisfiable without a TTY. Plus unit mutation on changed lines. Bonus: run_default's error branch is untested today, so this is a net coverage gain.
  • Integration: N/A — integration tests hit the SDK, never the CLI.
  • E2E, pipe path: ordinary subprocess with piped stdin (Rust tests/*_e2e.rs, python/ts e2e).
  • E2E, TTY path: curtaincall — real PTY + VT100. Banner renders, prompt appears, exit/quit terminate, term.key_ctrl_d() terminates. See Epic: bare dirsql opens an interactive REPL #953 for the Python-floor caveat.

Checklist

  • Red tests pushed as their own commit; CI observed red for the right reason before implementation
  • Changelog fragment under packages/rust/changelog.d/
  • Migration fragment under packages/rust/migrations.d/ — required: runtime behavior change with no API change (bare dirsql: exit-2 → REPL)
  • Docs: new section in docs/reference/cli.md, plus the three places stating "no subcommand and no SQL: a usage error" — run.rs module doc (~line 9), clap about (~line 25), clap long_about (~line 27)
  • PARITY.md: CLI is one shared implementation → no drift; state it in the PR rather than skipping silently
  • Re-attest both bindings (a packages/rust/src change stales python and ts)
  • Manual run per AGENTS.md — build it and actually type at the REPL

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions