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:
@@ -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",
|
||||
&[
|
||||
|
||||
@@ -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
121
src/cli/commands/tui.rs
Normal 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"));
|
||||
}
|
||||
}
|
||||
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user