feat(tui): Phase 4 completion + Phase 5 session/lock/text-width

Phase 4 (bd-1df9) — all 5 acceptance criteria met:
- Sync screen with delta ledger (bd-2x2h, bd-y095)
- Doctor screen with health checks (bd-2iqk)
- Stats screen with document counts (bd-2iqk)
- CLI integration: lore tui subcommand (bd-26lp)
- CLI integration: lore sync --tui flag (bd-3l56)

Phase 5 (bd-3h00) — session persistence + instance lock + text width:
- text_width.rs: Unicode-aware measurement, truncation, padding (16 tests)
- instance_lock.rs: Advisory PID lock with stale recovery (6 tests)
- session.rs: Atomic write + CRC32 checksum + quarantine (9 tests)

Closes: bd-26lp, bd-3h00, bd-3l56, bd-1df9, bd-y095
This commit is contained in:
teernisse
2026-02-18 23:40:30 -05:00
parent 418417b0f4
commit 146eb61623
45 changed files with 5216 additions and 207 deletions

View File

@@ -128,6 +128,7 @@ const COMMAND_FLAGS: &[(&str, &[&str])] = &[
"--dry-run",
"--no-dry-run",
"--timings",
"--tui",
],
),
(
@@ -256,6 +257,7 @@ const COMMAND_FLAGS: &[(&str, &[&str])] = &[
("generate-docs", &["--full", "--project"]),
("completions", &[]),
("robot-docs", &["--brief"]),
("tui", &["--config"]),
(
"list",
&[

View File

@@ -15,6 +15,7 @@ pub mod sync;
pub mod sync_status;
pub mod timeline;
pub mod trace;
pub mod tui;
pub mod who;
pub use auth_test::run_auth_test;
@@ -50,6 +51,7 @@ pub use sync::{SyncOptions, SyncResult, print_sync, print_sync_json, run_sync};
pub use sync_status::{print_sync_status, print_sync_status_json, run_sync_status};
pub use timeline::{TimelineParams, print_timeline, print_timeline_json_with_meta, run_timeline};
pub use trace::{parse_trace_path, print_trace, print_trace_json};
pub use tui::{TuiArgs, find_lore_tui, run_tui};
pub use who::{
WhoRun, half_life_decay, print_who_human, print_who_json, query_active, query_expert,
query_overlap, query_reviews, query_workload, run_who,

121
src/cli/commands/tui.rs Normal file
View File

@@ -0,0 +1,121 @@
//! `lore tui` subcommand — delegates to the `lore-tui` binary.
//!
//! Resolves `lore-tui` via PATH and execs it, replacing the current process.
//! In robot mode, returns a structured JSON error (TUI is human-only).
use std::path::PathBuf;
use clap::Parser;
/// Launch the interactive TUI dashboard
#[derive(Parser, Debug)]
pub struct TuiArgs {
/// Path to config file (forwarded to lore-tui)
#[arg(long)]
pub config: Option<String>,
}
/// Resolve the `lore-tui` binary via PATH lookup.
pub fn find_lore_tui() -> Option<PathBuf> {
which::which("lore-tui").ok()
}
/// Run the TUI subcommand.
///
/// In robot mode this returns an error (TUI requires a terminal).
/// Otherwise it execs `lore-tui`, replacing the current process.
pub fn run_tui(args: &TuiArgs, robot_mode: bool) -> Result<(), Box<dyn std::error::Error>> {
if robot_mode {
let err = serde_json::json!({
"error": {
"code": "TUI_NOT_AVAILABLE",
"message": "The TUI requires an interactive terminal and cannot run in robot mode.",
"suggestion": "Use `lore --robot <command>` for programmatic access.",
"actions": []
}
});
eprintln!("{err}");
std::process::exit(2);
}
let binary = find_lore_tui().ok_or_else(|| {
"Could not find `lore-tui` on PATH.\n\n\
Install it with:\n \
cargo install --path crates/lore-tui\n\n\
Or build the workspace:\n \
cargo build --release -p lore-tui"
.to_string()
})?;
// Build the command with explicit arguments (no shell interpolation).
let mut cmd = std::process::Command::new(&binary);
if let Some(ref config) = args.config {
cmd.arg("--config").arg(config);
}
// On Unix, exec() replaces the current process entirely.
// This gives lore-tui direct terminal control (stdin/stdout/stderr).
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
let err = cmd.exec();
// exec() only returns on error
Err(format!("Failed to exec lore-tui at {}: {err}", binary.display()).into())
}
// On non-Unix, spawn and wait.
#[cfg(not(unix))]
{
let status = cmd.status()?;
if status.success() {
Ok(())
} else {
std::process::exit(status.code().unwrap_or(1));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_lore_tui_does_not_panic() {
// Just verify the lookup doesn't panic; it may or may not find the binary.
let _ = find_lore_tui();
}
#[test]
fn test_robot_mode_error_json_structure() {
let err = serde_json::json!({
"error": {
"code": "TUI_NOT_AVAILABLE",
"message": "The TUI requires an interactive terminal and cannot run in robot mode.",
"suggestion": "Use `lore --robot <command>` for programmatic access.",
"actions": []
}
});
let parsed: serde_json::Value = serde_json::from_str(&err.to_string()).unwrap();
assert_eq!(parsed["error"]["code"], "TUI_NOT_AVAILABLE");
}
#[test]
fn test_tui_args_default() {
let args = TuiArgs { config: None };
assert!(args.config.is_none());
}
#[test]
fn test_tui_args_with_config() {
let args = TuiArgs {
config: Some("/tmp/test.json".into()),
};
assert_eq!(args.config.as_deref(), Some("/tmp/test.json"));
}
#[test]
fn test_binary_not_found_error_message() {
let msg = "Could not find `lore-tui` on PATH.";
assert!(msg.contains("lore-tui"));
}
}

View File

@@ -7,6 +7,8 @@ pub mod robot;
use clap::{Parser, Subcommand};
use std::io::IsTerminal;
use commands::tui::TuiArgs;
#[derive(Parser)]
#[command(name = "lore")]
#[command(version = env!("LORE_VERSION"), about = "Local GitLab data management with semantic search", long_about = None)]
@@ -241,6 +243,9 @@ pub enum Commands {
/// Trace why code was introduced: file -> MR -> issue -> discussion
Trace(TraceArgs),
/// Launch the interactive TUI dashboard
Tui(TuiArgs),
/// Detect discussion divergence from original intent
Drift {
/// Entity type (currently only "issues" supported)
@@ -805,6 +810,10 @@ pub struct SyncArgs {
/// Show detailed timing breakdown for sync stages
#[arg(short = 't', long = "timings")]
pub timings: bool,
/// Show sync progress in interactive TUI
#[arg(long)]
pub tui: bool,
}
#[derive(Parser)]