Compare commits
7 Commits
41d20f1374
...
51c370fac2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51c370fac2 | ||
|
|
7b7d781a19 | ||
|
|
03ea51513d | ||
|
|
667f70e177 | ||
|
|
585b746461 | ||
|
|
730ddef339 | ||
|
|
5508d8464a |
15
AGENTS.md
15
AGENTS.md
@@ -98,8 +98,8 @@ Errors return structured JSON to stderr:
|
|||||||
| Code | Meaning |
|
| Code | Meaning |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| 0 | Success |
|
| 0 | Success |
|
||||||
| 1 | Internal error |
|
| 1 | Internal error / health check failed / not implemented |
|
||||||
| 2 | Config not found |
|
| 2 | Usage error (invalid flags or arguments) |
|
||||||
| 3 | Config invalid |
|
| 3 | Config invalid |
|
||||||
| 4 | Token not set |
|
| 4 | Token not set |
|
||||||
| 5 | GitLab auth failed |
|
| 5 | GitLab auth failed |
|
||||||
@@ -111,6 +111,17 @@ Errors return structured JSON to stderr:
|
|||||||
| 11 | Migration failed |
|
| 11 | Migration failed |
|
||||||
| 12 | I/O error |
|
| 12 | I/O error |
|
||||||
| 13 | Transform error |
|
| 13 | Transform error |
|
||||||
|
| 14 | Ollama unavailable |
|
||||||
|
| 15 | Ollama model not found |
|
||||||
|
| 16 | Embedding failed |
|
||||||
|
| 20 | Config not found |
|
||||||
|
|
||||||
|
### Configuration Precedence
|
||||||
|
|
||||||
|
1. CLI flags (highest priority)
|
||||||
|
2. Environment variables (`LORE_ROBOT`, `GITLAB_TOKEN`, `LORE_CONFIG_PATH`)
|
||||||
|
3. Config file (`~/.config/lore/config.json`)
|
||||||
|
4. Built-in defaults (lowest priority)
|
||||||
|
|
||||||
### Best Practices
|
### Best Practices
|
||||||
|
|
||||||
|
|||||||
11
Cargo.lock
generated
11
Cargo.lock
generated
@@ -211,6 +211,15 @@ dependencies = [
|
|||||||
"strsim",
|
"strsim",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_complete"
|
||||||
|
version = "4.5.65"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "430b4dc2b5e3861848de79627b2bedc9f3342c7da5173a14eaa5d0f8dc18ae5d"
|
||||||
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_derive"
|
name = "clap_derive"
|
||||||
version = "4.5.49"
|
version = "4.5.49"
|
||||||
@@ -1085,6 +1094,7 @@ dependencies = [
|
|||||||
"async-stream",
|
"async-stream",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
"clap_complete",
|
||||||
"comfy-table",
|
"comfy-table",
|
||||||
"console",
|
"console",
|
||||||
"dialoguer",
|
"dialoguer",
|
||||||
@@ -1092,6 +1102,7 @@ dependencies = [
|
|||||||
"flate2",
|
"flate2",
|
||||||
"futures",
|
"futures",
|
||||||
"indicatif",
|
"indicatif",
|
||||||
|
"libc",
|
||||||
"open",
|
"open",
|
||||||
"rand",
|
"rand",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ serde_json = "1"
|
|||||||
|
|
||||||
# CLI
|
# CLI
|
||||||
clap = { version = "4", features = ["derive", "env"] }
|
clap = { version = "4", features = ["derive", "env"] }
|
||||||
|
clap_complete = "4"
|
||||||
dialoguer = "0.12"
|
dialoguer = "0.12"
|
||||||
console = "0.16"
|
console = "0.16"
|
||||||
indicatif = "0.18"
|
indicatif = "0.18"
|
||||||
@@ -46,6 +47,9 @@ flate2 = "1"
|
|||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
|
|
||||||
|
[target.'cfg(unix)'.dependencies]
|
||||||
|
libc = "0.2"
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|||||||
35
README.md
35
README.md
@@ -406,8 +406,8 @@ Errors return structured JSON to stderr:
|
|||||||
| Code | Meaning |
|
| Code | Meaning |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| 0 | Success |
|
| 0 | Success |
|
||||||
| 1 | Internal error |
|
| 1 | Internal error / health check failed / not implemented |
|
||||||
| 2 | Config not found |
|
| 2 | Usage error (invalid flags or arguments) |
|
||||||
| 3 | Config invalid |
|
| 3 | Config invalid |
|
||||||
| 4 | Token not set |
|
| 4 | Token not set |
|
||||||
| 5 | GitLab auth failed |
|
| 5 | GitLab auth failed |
|
||||||
@@ -419,6 +419,19 @@ Errors return structured JSON to stderr:
|
|||||||
| 11 | Migration failed |
|
| 11 | Migration failed |
|
||||||
| 12 | I/O error |
|
| 12 | I/O error |
|
||||||
| 13 | Transform error |
|
| 13 | Transform error |
|
||||||
|
| 14 | Ollama unavailable |
|
||||||
|
| 15 | Ollama model not found |
|
||||||
|
| 16 | Embedding failed |
|
||||||
|
| 20 | Config not found |
|
||||||
|
|
||||||
|
## Configuration Precedence
|
||||||
|
|
||||||
|
Settings are resolved in this order (highest to lowest priority):
|
||||||
|
|
||||||
|
1. CLI flags (`--robot`, `--config`, `--color`)
|
||||||
|
2. Environment variables (`LORE_ROBOT`, `GITLAB_TOKEN`, `LORE_CONFIG_PATH`)
|
||||||
|
3. Config file (`~/.config/lore/config.json`)
|
||||||
|
4. Built-in defaults
|
||||||
|
|
||||||
## Global Options
|
## Global Options
|
||||||
|
|
||||||
@@ -428,6 +441,24 @@ lore --robot <command> # Machine-readable JSON
|
|||||||
lore -J <command> # JSON shorthand
|
lore -J <command> # JSON shorthand
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Shell Completions
|
||||||
|
|
||||||
|
Generate shell completions for tab-completion support:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Bash (add to ~/.bashrc)
|
||||||
|
lore completions bash > ~/.local/share/bash-completion/completions/lore
|
||||||
|
|
||||||
|
# Zsh (add to ~/.zshrc: fpath=(~/.zfunc $fpath))
|
||||||
|
lore completions zsh > ~/.zfunc/_lore
|
||||||
|
|
||||||
|
# Fish
|
||||||
|
lore completions fish > ~/.config/fish/completions/lore.fish
|
||||||
|
|
||||||
|
# PowerShell (add to $PROFILE)
|
||||||
|
lore completions powershell >> $PROFILE
|
||||||
|
```
|
||||||
|
|
||||||
## Database Schema
|
## Database Schema
|
||||||
|
|
||||||
Data is stored in SQLite with WAL mode and foreign keys enabled. Main tables:
|
Data is stored in SQLite with WAL mode and foreign keys enabled. Main tables:
|
||||||
|
|||||||
10
build.rs
Normal file
10
build.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
fn main() {
|
||||||
|
let hash = std::process::Command::new("git")
|
||||||
|
.args(["rev-parse", "--short", "HEAD"])
|
||||||
|
.output()
|
||||||
|
.ok()
|
||||||
|
.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||||
|
.unwrap_or_default();
|
||||||
|
println!("cargo:rustc-env=GIT_HASH={}", hash.trim());
|
||||||
|
println!("cargo:rerun-if-changed=.git/HEAD");
|
||||||
|
}
|
||||||
@@ -8,7 +8,8 @@ use tracing::info;
|
|||||||
use crate::core::db::create_connection;
|
use crate::core::db::create_connection;
|
||||||
use crate::core::error::Result;
|
use crate::core::error::Result;
|
||||||
use crate::core::paths::get_db_path;
|
use crate::core::paths::get_db_path;
|
||||||
use crate::documents::{regenerate_dirty_documents, SourceType};
|
use crate::core::project::resolve_project;
|
||||||
|
use crate::documents::{SourceType, regenerate_dirty_documents};
|
||||||
use crate::Config;
|
use crate::Config;
|
||||||
|
|
||||||
const FULL_MODE_CHUNK_SIZE: i64 = 2000;
|
const FULL_MODE_CHUNK_SIZE: i64 = 2000;
|
||||||
@@ -81,18 +82,7 @@ fn seed_dirty(
|
|||||||
|
|
||||||
loop {
|
loop {
|
||||||
let inserted = if let Some(project) = project_filter {
|
let inserted = if let Some(project) = project_filter {
|
||||||
// Resolve project to ID for filtering
|
let project_id = resolve_project(conn, project)?;
|
||||||
let project_id: Option<i64> = conn
|
|
||||||
.query_row(
|
|
||||||
"SELECT id FROM projects WHERE path_with_namespace = ?1 COLLATE NOCASE",
|
|
||||||
[project],
|
|
||||||
|row| row.get(0),
|
|
||||||
)
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
let Some(pid) = project_id else {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
&format!(
|
&format!(
|
||||||
@@ -101,7 +91,7 @@ fn seed_dirty(
|
|||||||
FROM {table} WHERE id > ?3 AND project_id = ?4 ORDER BY id LIMIT ?5
|
FROM {table} WHERE id > ?3 AND project_id = ?4 ORDER BY id LIMIT ?5
|
||||||
ON CONFLICT(source_type, source_id) DO NOTHING"
|
ON CONFLICT(source_type, source_id) DO NOTHING"
|
||||||
),
|
),
|
||||||
rusqlite::params![type_str, now, last_id, pid, FULL_MODE_CHUNK_SIZE],
|
rusqlite::params![type_str, now, last_id, project_id, FULL_MODE_CHUNK_SIZE],
|
||||||
)?
|
)?
|
||||||
} else {
|
} else {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use crate::core::db::create_connection;
|
|||||||
use crate::core::error::{LoreError, Result};
|
use crate::core::error::{LoreError, Result};
|
||||||
use crate::core::lock::{AppLock, LockOptions};
|
use crate::core::lock::{AppLock, LockOptions};
|
||||||
use crate::core::paths::get_db_path;
|
use crate::core::paths::get_db_path;
|
||||||
|
use crate::core::project::resolve_project;
|
||||||
use crate::gitlab::GitLabClient;
|
use crate::gitlab::GitLabClient;
|
||||||
use crate::ingestion::{
|
use crate::ingestion::{
|
||||||
IngestMrProjectResult, IngestProjectResult, ProgressEvent, ingest_project_issues_with_progress,
|
IngestMrProjectResult, IngestProjectResult, ProgressEvent, ingest_project_issues_with_progress,
|
||||||
@@ -40,6 +41,36 @@ pub struct IngestResult {
|
|||||||
pub notes_upserted: usize,
|
pub notes_upserted: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Controls what interactive UI elements `run_ingest` displays.
|
||||||
|
///
|
||||||
|
/// Separates progress indicators (spinners, bars) from text output (headers,
|
||||||
|
/// per-project summaries) so callers like `sync` can show progress without
|
||||||
|
/// duplicating summary text.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct IngestDisplay {
|
||||||
|
/// Show animated spinners and progress bars.
|
||||||
|
pub show_progress: bool,
|
||||||
|
/// Show text headers ("Ingesting...") and per-project summary lines.
|
||||||
|
pub show_text: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IngestDisplay {
|
||||||
|
/// Interactive mode: everything visible.
|
||||||
|
pub fn interactive() -> Self {
|
||||||
|
Self { show_progress: true, show_text: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Robot/JSON mode: everything hidden.
|
||||||
|
pub fn silent() -> Self {
|
||||||
|
Self { show_progress: false, show_text: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Progress only (used by sync in interactive mode).
|
||||||
|
pub fn progress_only() -> Self {
|
||||||
|
Self { show_progress: true, show_text: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Run the ingest command.
|
/// Run the ingest command.
|
||||||
pub async fn run_ingest(
|
pub async fn run_ingest(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
@@ -47,7 +78,7 @@ pub async fn run_ingest(
|
|||||||
project_filter: Option<&str>,
|
project_filter: Option<&str>,
|
||||||
force: bool,
|
force: bool,
|
||||||
full: bool,
|
full: bool,
|
||||||
robot_mode: bool,
|
display: IngestDisplay,
|
||||||
) -> Result<IngestResult> {
|
) -> Result<IngestResult> {
|
||||||
// Validate resource type early
|
// Validate resource type early
|
||||||
if resource_type != "issues" && resource_type != "mrs" {
|
if resource_type != "issues" && resource_type != "mrs" {
|
||||||
@@ -86,7 +117,7 @@ pub async fn run_ingest(
|
|||||||
|
|
||||||
// If --full flag is set, reset sync cursors and discussion watermarks for a complete re-fetch
|
// If --full flag is set, reset sync cursors and discussion watermarks for a complete re-fetch
|
||||||
if full {
|
if full {
|
||||||
if !robot_mode {
|
if display.show_text {
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
style("Full sync: resetting cursors to fetch all data...").yellow()
|
style("Full sync: resetting cursors to fetch all data...").yellow()
|
||||||
@@ -139,7 +170,7 @@ pub async fn run_ingest(
|
|||||||
} else {
|
} else {
|
||||||
"merge requests"
|
"merge requests"
|
||||||
};
|
};
|
||||||
if !robot_mode {
|
if display.show_text {
|
||||||
println!("{}", style(format!("Ingesting {type_label}...")).blue());
|
println!("{}", style(format!("Ingesting {type_label}...")).blue());
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
@@ -147,7 +178,7 @@ pub async fn run_ingest(
|
|||||||
// Sync each project
|
// Sync each project
|
||||||
for (local_project_id, gitlab_project_id, path) in &projects {
|
for (local_project_id, gitlab_project_id, path) in &projects {
|
||||||
// Show spinner while fetching (only in interactive mode)
|
// Show spinner while fetching (only in interactive mode)
|
||||||
let spinner = if robot_mode {
|
let spinner = if !display.show_progress {
|
||||||
ProgressBar::hidden()
|
ProgressBar::hidden()
|
||||||
} else {
|
} else {
|
||||||
let s = ProgressBar::new_spinner();
|
let s = ProgressBar::new_spinner();
|
||||||
@@ -162,7 +193,7 @@ pub async fn run_ingest(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Progress bar for discussion sync (hidden until needed, or always hidden in robot mode)
|
// Progress bar for discussion sync (hidden until needed, or always hidden in robot mode)
|
||||||
let disc_bar = if robot_mode {
|
let disc_bar = if !display.show_progress {
|
||||||
ProgressBar::hidden()
|
ProgressBar::hidden()
|
||||||
} else {
|
} else {
|
||||||
let b = ProgressBar::new(0);
|
let b = ProgressBar::new(0);
|
||||||
@@ -178,7 +209,7 @@ pub async fn run_ingest(
|
|||||||
// Create progress callback (no-op in robot mode)
|
// Create progress callback (no-op in robot mode)
|
||||||
let spinner_clone = spinner.clone();
|
let spinner_clone = spinner.clone();
|
||||||
let disc_bar_clone = disc_bar.clone();
|
let disc_bar_clone = disc_bar.clone();
|
||||||
let progress_callback: crate::ingestion::ProgressCallback = if robot_mode {
|
let progress_callback: crate::ingestion::ProgressCallback = if !display.show_progress {
|
||||||
Box::new(|_| {})
|
Box::new(|_| {})
|
||||||
} else {
|
} else {
|
||||||
Box::new(move |event: ProgressEvent| match event {
|
Box::new(move |event: ProgressEvent| match event {
|
||||||
@@ -225,7 +256,7 @@ pub async fn run_ingest(
|
|||||||
disc_bar.finish_and_clear();
|
disc_bar.finish_and_clear();
|
||||||
|
|
||||||
// Print per-project summary (only in interactive mode)
|
// Print per-project summary (only in interactive mode)
|
||||||
if !robot_mode {
|
if display.show_text {
|
||||||
print_issue_project_summary(path, &result);
|
print_issue_project_summary(path, &result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +285,7 @@ pub async fn run_ingest(
|
|||||||
disc_bar.finish_and_clear();
|
disc_bar.finish_and_clear();
|
||||||
|
|
||||||
// Print per-project summary (only in interactive mode)
|
// Print per-project summary (only in interactive mode)
|
||||||
if !robot_mode {
|
if display.show_text {
|
||||||
print_mr_project_summary(path, &result);
|
print_mr_project_summary(path, &result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,16 +314,39 @@ fn get_projects_to_sync(
|
|||||||
configured_projects: &[crate::core::config::ProjectConfig],
|
configured_projects: &[crate::core::config::ProjectConfig],
|
||||||
filter: Option<&str>,
|
filter: Option<&str>,
|
||||||
) -> Result<Vec<(i64, i64, String)>> {
|
) -> Result<Vec<(i64, i64, String)>> {
|
||||||
let mut projects = Vec::new();
|
// If a filter is provided, resolve it to a specific project
|
||||||
|
if let Some(filter_str) = filter {
|
||||||
|
let project_id = resolve_project(conn, filter_str)?;
|
||||||
|
|
||||||
for project_config in configured_projects {
|
// Verify the resolved project is in our config
|
||||||
if let Some(filter_path) = filter
|
let row: Option<(i64, String)> = conn
|
||||||
&& !project_config.path.contains(filter_path)
|
.query_row(
|
||||||
{
|
"SELECT gitlab_project_id, path_with_namespace FROM projects WHERE id = ?1",
|
||||||
continue;
|
[project_id],
|
||||||
|
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||||
|
)
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
if let Some((gitlab_id, path)) = row {
|
||||||
|
// Confirm it's a configured project
|
||||||
|
if configured_projects.iter().any(|p| p.path == path) {
|
||||||
|
return Ok(vec![(project_id, gitlab_id, path)]);
|
||||||
|
}
|
||||||
|
return Err(LoreError::Other(format!(
|
||||||
|
"Project '{}' exists in database but is not in configuration",
|
||||||
|
path
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get project from database
|
return Err(LoreError::Other(format!(
|
||||||
|
"Project '{}' not found in database",
|
||||||
|
filter_str
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// No filter: return all configured projects
|
||||||
|
let mut projects = Vec::new();
|
||||||
|
for project_config in configured_projects {
|
||||||
let result: Option<(i64, i64)> = conn
|
let result: Option<(i64, i64)> = conn
|
||||||
.query_row(
|
.query_row(
|
||||||
"SELECT id, gitlab_project_id FROM projects WHERE path_with_namespace = ?",
|
"SELECT id, gitlab_project_id FROM projects WHERE path_with_namespace = ?",
|
||||||
|
|||||||
@@ -6,10 +6,21 @@ use serde::Serialize;
|
|||||||
|
|
||||||
use crate::Config;
|
use crate::Config;
|
||||||
use crate::core::db::create_connection;
|
use crate::core::db::create_connection;
|
||||||
use crate::core::error::Result;
|
use crate::core::error::{LoreError, Result};
|
||||||
use crate::core::paths::get_db_path;
|
use crate::core::paths::get_db_path;
|
||||||
|
use crate::core::project::resolve_project;
|
||||||
use crate::core::time::{ms_to_iso, now_ms, parse_since};
|
use crate::core::time::{ms_to_iso, now_ms, parse_since};
|
||||||
|
|
||||||
|
/// Apply foreground color to a Cell only if colors are enabled.
|
||||||
|
fn colored_cell(content: impl std::fmt::Display, color: Color) -> Cell {
|
||||||
|
let cell = Cell::new(content);
|
||||||
|
if console::colors_enabled() {
|
||||||
|
cell.fg(color)
|
||||||
|
} else {
|
||||||
|
cell
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Issue row for display.
|
/// Issue row for display.
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct IssueListRow {
|
pub struct IssueListRow {
|
||||||
@@ -232,11 +243,9 @@ fn query_issues(conn: &Connection, filters: &ListFilters) -> Result<ListResult>
|
|||||||
let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
|
let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
|
||||||
|
|
||||||
if let Some(project) = filters.project {
|
if let Some(project) = filters.project {
|
||||||
// Exact match or suffix match after '/' to avoid partial matches
|
let project_id = resolve_project(conn, project)?;
|
||||||
// e.g. "foo" matches "group/foo" but NOT "group/foobar"
|
where_clauses.push("i.project_id = ?");
|
||||||
where_clauses.push("(p.path_with_namespace = ? OR p.path_with_namespace LIKE ?)");
|
params.push(Box::new(project_id));
|
||||||
params.push(Box::new(project.to_string()));
|
|
||||||
params.push(Box::new(format!("%/{project}")));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(state) = filters.state
|
if let Some(state) = filters.state
|
||||||
@@ -264,9 +273,13 @@ fn query_issues(conn: &Connection, filters: &ListFilters) -> Result<ListResult>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle since filter
|
// Handle since filter
|
||||||
if let Some(since_str) = filters.since
|
if let Some(since_str) = filters.since {
|
||||||
&& let Some(cutoff_ms) = parse_since(since_str)
|
let cutoff_ms = parse_since(since_str).ok_or_else(|| {
|
||||||
{
|
LoreError::Other(format!(
|
||||||
|
"Invalid --since value '{}'. Use relative (7d, 2w, 1m) or absolute (YYYY-MM-DD) format.",
|
||||||
|
since_str
|
||||||
|
))
|
||||||
|
})?;
|
||||||
where_clauses.push("i.updated_at >= ?");
|
where_clauses.push("i.updated_at >= ?");
|
||||||
params.push(Box::new(cutoff_ms));
|
params.push(Box::new(cutoff_ms));
|
||||||
}
|
}
|
||||||
@@ -419,11 +432,9 @@ fn query_mrs(conn: &Connection, filters: &MrListFilters) -> Result<MrListResult>
|
|||||||
let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
|
let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
|
||||||
|
|
||||||
if let Some(project) = filters.project {
|
if let Some(project) = filters.project {
|
||||||
// Exact match or suffix match after '/' to avoid partial matches
|
let project_id = resolve_project(conn, project)?;
|
||||||
// e.g. "foo" matches "group/foo" but NOT "group/foobar"
|
where_clauses.push("m.project_id = ?");
|
||||||
where_clauses.push("(p.path_with_namespace = ? OR p.path_with_namespace LIKE ?)");
|
params.push(Box::new(project_id));
|
||||||
params.push(Box::new(project.to_string()));
|
|
||||||
params.push(Box::new(format!("%/{project}")));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(state) = filters.state
|
if let Some(state) = filters.state
|
||||||
@@ -461,9 +472,13 @@ fn query_mrs(conn: &Connection, filters: &MrListFilters) -> Result<MrListResult>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle since filter
|
// Handle since filter
|
||||||
if let Some(since_str) = filters.since
|
if let Some(since_str) = filters.since {
|
||||||
&& let Some(cutoff_ms) = parse_since(since_str)
|
let cutoff_ms = parse_since(since_str).ok_or_else(|| {
|
||||||
{
|
LoreError::Other(format!(
|
||||||
|
"Invalid --since value '{}'. Use relative (7d, 2w, 1m) or absolute (YYYY-MM-DD) format.",
|
||||||
|
since_str
|
||||||
|
))
|
||||||
|
})?;
|
||||||
where_clauses.push("m.updated_at >= ?");
|
where_clauses.push("m.updated_at >= ?");
|
||||||
params.push(Box::new(cutoff_ms));
|
params.push(Box::new(cutoff_ms));
|
||||||
}
|
}
|
||||||
@@ -628,10 +643,22 @@ fn format_relative_time(ms_epoch: i64) -> String {
|
|||||||
match diff {
|
match diff {
|
||||||
d if d < 60_000 => "just now".to_string(),
|
d if d < 60_000 => "just now".to_string(),
|
||||||
d if d < 3_600_000 => format!("{} min ago", d / 60_000),
|
d if d < 3_600_000 => format!("{} min ago", d / 60_000),
|
||||||
d if d < 86_400_000 => format!("{} hours ago", d / 3_600_000),
|
d if d < 86_400_000 => {
|
||||||
d if d < 604_800_000 => format!("{} days ago", d / 86_400_000),
|
let n = d / 3_600_000;
|
||||||
d if d < 2_592_000_000 => format!("{} weeks ago", d / 604_800_000),
|
format!("{n} {} ago", if n == 1 { "hour" } else { "hours" })
|
||||||
_ => format!("{} months ago", diff / 2_592_000_000),
|
}
|
||||||
|
d if d < 604_800_000 => {
|
||||||
|
let n = d / 86_400_000;
|
||||||
|
format!("{n} {} ago", if n == 1 { "day" } else { "days" })
|
||||||
|
}
|
||||||
|
d if d < 2_592_000_000 => {
|
||||||
|
let n = d / 604_800_000;
|
||||||
|
format!("{n} {} ago", if n == 1 { "week" } else { "weeks" })
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let n = diff / 2_592_000_000;
|
||||||
|
format!("{n} {} ago", if n == 1 { "month" } else { "months" })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -735,19 +762,19 @@ pub fn print_list_issues(result: &ListResult) {
|
|||||||
let discussions = format_discussions(issue.discussion_count, issue.unresolved_count);
|
let discussions = format_discussions(issue.discussion_count, issue.unresolved_count);
|
||||||
|
|
||||||
let state_cell = if issue.state == "opened" {
|
let state_cell = if issue.state == "opened" {
|
||||||
Cell::new(&issue.state).fg(Color::Green)
|
colored_cell(&issue.state, Color::Green)
|
||||||
} else {
|
} else {
|
||||||
Cell::new(&issue.state).fg(Color::DarkGrey)
|
colored_cell(&issue.state, Color::DarkGrey)
|
||||||
};
|
};
|
||||||
|
|
||||||
table.add_row(vec![
|
table.add_row(vec![
|
||||||
Cell::new(format!("#{}", issue.iid)).fg(Color::Cyan),
|
colored_cell(format!("#{}", issue.iid), Color::Cyan),
|
||||||
Cell::new(title),
|
Cell::new(title),
|
||||||
state_cell,
|
state_cell,
|
||||||
Cell::new(assignee).fg(Color::Magenta),
|
colored_cell(assignee, Color::Magenta),
|
||||||
Cell::new(labels).fg(Color::Yellow),
|
colored_cell(labels, Color::Yellow),
|
||||||
Cell::new(discussions),
|
Cell::new(discussions),
|
||||||
Cell::new(relative_time).fg(Color::DarkGrey),
|
colored_cell(relative_time, Color::DarkGrey),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -807,7 +834,6 @@ pub fn print_list_mrs(result: &MrListResult) {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
for mr in &result.mrs {
|
for mr in &result.mrs {
|
||||||
// Add [DRAFT] prefix for draft MRs
|
|
||||||
let title = if mr.draft {
|
let title = if mr.draft {
|
||||||
format!("[DRAFT] {}", truncate_with_ellipsis(&mr.title, 38))
|
format!("[DRAFT] {}", truncate_with_ellipsis(&mr.title, 38))
|
||||||
} else {
|
} else {
|
||||||
@@ -819,25 +845,24 @@ pub fn print_list_mrs(result: &MrListResult) {
|
|||||||
let discussions = format_discussions(mr.discussion_count, mr.unresolved_count);
|
let discussions = format_discussions(mr.discussion_count, mr.unresolved_count);
|
||||||
|
|
||||||
let state_cell = match mr.state.as_str() {
|
let state_cell = match mr.state.as_str() {
|
||||||
"opened" => Cell::new(&mr.state).fg(Color::Green),
|
"opened" => colored_cell(&mr.state, Color::Green),
|
||||||
"merged" => Cell::new(&mr.state).fg(Color::Magenta),
|
"merged" => colored_cell(&mr.state, Color::Magenta),
|
||||||
"closed" => Cell::new(&mr.state).fg(Color::Red),
|
"closed" => colored_cell(&mr.state, Color::Red),
|
||||||
"locked" => Cell::new(&mr.state).fg(Color::Yellow),
|
"locked" => colored_cell(&mr.state, Color::Yellow),
|
||||||
_ => Cell::new(&mr.state).fg(Color::DarkGrey),
|
_ => colored_cell(&mr.state, Color::DarkGrey),
|
||||||
};
|
};
|
||||||
|
|
||||||
table.add_row(vec![
|
table.add_row(vec![
|
||||||
Cell::new(format!("!{}", mr.iid)).fg(Color::Cyan),
|
colored_cell(format!("!{}", mr.iid), Color::Cyan),
|
||||||
Cell::new(title),
|
Cell::new(title),
|
||||||
state_cell,
|
state_cell,
|
||||||
Cell::new(format!(
|
colored_cell(
|
||||||
"@{}",
|
format!("@{}", truncate_with_ellipsis(&mr.author_username, 12)),
|
||||||
truncate_with_ellipsis(&mr.author_username, 12)
|
Color::Magenta,
|
||||||
))
|
),
|
||||||
.fg(Color::Magenta),
|
colored_cell(branches, Color::Blue),
|
||||||
Cell::new(branches).fg(Color::Blue),
|
|
||||||
Cell::new(discussions),
|
Cell::new(discussions),
|
||||||
Cell::new(relative_time).fg(Color::DarkGrey),
|
colored_cell(relative_time, Color::DarkGrey),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ pub use stats::{print_stats, print_stats_json, run_stats};
|
|||||||
pub use search::{
|
pub use search::{
|
||||||
print_search_results, print_search_results_json, run_search, SearchCliFilters, SearchResponse,
|
print_search_results, print_search_results_json, run_search, SearchCliFilters, SearchResponse,
|
||||||
};
|
};
|
||||||
pub use ingest::{print_ingest_summary, print_ingest_summary_json, run_ingest};
|
pub use ingest::{IngestDisplay, print_ingest_summary, print_ingest_summary_json, run_ingest};
|
||||||
pub use init::{InitInputs, InitOptions, InitResult, run_init};
|
pub use init::{InitInputs, InitOptions, InitResult, run_init};
|
||||||
pub use list::{
|
pub use list::{
|
||||||
ListFilters, MrListFilters, open_issue_in_browser, open_mr_in_browser, print_list_issues,
|
ListFilters, MrListFilters, open_issue_in_browser, open_mr_in_browser, print_list_issues,
|
||||||
|
|||||||
@@ -104,8 +104,30 @@ pub fn run_search(
|
|||||||
.map(|p| resolve_project(&conn, p))
|
.map(|p| resolve_project(&conn, p))
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
|
||||||
let after = cli_filters.after.as_deref().and_then(parse_since);
|
let after = cli_filters
|
||||||
let updated_after = cli_filters.updated_after.as_deref().and_then(parse_since);
|
.after
|
||||||
|
.as_deref()
|
||||||
|
.map(|s| {
|
||||||
|
parse_since(s).ok_or_else(|| {
|
||||||
|
LoreError::Other(format!(
|
||||||
|
"Invalid --after value '{}'. Use relative (7d, 2w, 1m) or absolute (YYYY-MM-DD) format.",
|
||||||
|
s
|
||||||
|
))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
|
let updated_after = cli_filters
|
||||||
|
.updated_after
|
||||||
|
.as_deref()
|
||||||
|
.map(|s| {
|
||||||
|
parse_since(s).ok_or_else(|| {
|
||||||
|
LoreError::Other(format!(
|
||||||
|
"Invalid --updated-after value '{}'. Use relative (7d, 2w, 1m) or absolute (YYYY-MM-DD) format.",
|
||||||
|
s
|
||||||
|
))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
let path = cli_filters.path.as_deref().map(|p| {
|
let path = cli_filters.path.as_deref().map(|p| {
|
||||||
if p.ends_with('/') {
|
if p.ends_with('/') {
|
||||||
@@ -192,7 +214,7 @@ pub fn run_search(
|
|||||||
results.push(SearchResultDisplay {
|
results.push(SearchResultDisplay {
|
||||||
document_id: row.document_id,
|
document_id: row.document_id,
|
||||||
source_type: row.source_type.clone(),
|
source_type: row.source_type.clone(),
|
||||||
title: row.title.clone(),
|
title: row.title.clone().unwrap_or_default(),
|
||||||
url: row.url.clone(),
|
url: row.url.clone(),
|
||||||
author: row.author.clone(),
|
author: row.author.clone(),
|
||||||
created_at: row.created_at.map(ms_to_iso),
|
created_at: row.created_at.map(ms_to_iso),
|
||||||
@@ -219,7 +241,7 @@ pub fn run_search(
|
|||||||
struct HydratedRow {
|
struct HydratedRow {
|
||||||
document_id: i64,
|
document_id: i64,
|
||||||
source_type: String,
|
source_type: String,
|
||||||
title: String,
|
title: Option<String>,
|
||||||
url: Option<String>,
|
url: Option<String>,
|
||||||
author: Option<String>,
|
author: Option<String>,
|
||||||
created_at: Option<i64>,
|
created_at: Option<i64>,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use crate::Config;
|
|||||||
use crate::core::db::create_connection;
|
use crate::core::db::create_connection;
|
||||||
use crate::core::error::{LoreError, Result};
|
use crate::core::error::{LoreError, Result};
|
||||||
use crate::core::paths::get_db_path;
|
use crate::core::paths::get_db_path;
|
||||||
|
use crate::core::project::resolve_project;
|
||||||
use crate::core::time::ms_to_iso;
|
use crate::core::time::ms_to_iso;
|
||||||
|
|
||||||
/// Merge request metadata for display.
|
/// Merge request metadata for display.
|
||||||
@@ -145,18 +146,20 @@ struct IssueRow {
|
|||||||
/// Find issue by iid, optionally filtered by project.
|
/// Find issue by iid, optionally filtered by project.
|
||||||
fn find_issue(conn: &Connection, iid: i64, project_filter: Option<&str>) -> Result<IssueRow> {
|
fn find_issue(conn: &Connection, iid: i64, project_filter: Option<&str>) -> Result<IssueRow> {
|
||||||
let (sql, params): (&str, Vec<Box<dyn rusqlite::ToSql>>) = match project_filter {
|
let (sql, params): (&str, Vec<Box<dyn rusqlite::ToSql>>) = match project_filter {
|
||||||
Some(project) => (
|
Some(project) => {
|
||||||
|
let project_id = resolve_project(conn, project)?;
|
||||||
|
(
|
||||||
"SELECT i.id, i.iid, i.title, i.description, i.state, i.author_username,
|
"SELECT i.id, i.iid, i.title, i.description, i.state, i.author_username,
|
||||||
i.created_at, i.updated_at, i.web_url, p.path_with_namespace
|
i.created_at, i.updated_at, i.web_url, p.path_with_namespace
|
||||||
FROM issues i
|
FROM issues i
|
||||||
JOIN projects p ON i.project_id = p.id
|
JOIN projects p ON i.project_id = p.id
|
||||||
WHERE i.iid = ? AND (p.path_with_namespace = ? OR p.path_with_namespace LIKE ?)",
|
WHERE i.iid = ? AND i.project_id = ?",
|
||||||
vec![
|
vec![
|
||||||
Box::new(iid),
|
Box::new(iid),
|
||||||
Box::new(project.to_string()),
|
Box::new(project_id),
|
||||||
Box::new(format!("%/{}", project)),
|
|
||||||
],
|
],
|
||||||
),
|
)
|
||||||
|
}
|
||||||
None => (
|
None => (
|
||||||
"SELECT i.id, i.iid, i.title, i.description, i.state, i.author_username,
|
"SELECT i.id, i.iid, i.title, i.description, i.state, i.author_username,
|
||||||
i.created_at, i.updated_at, i.web_url, p.path_with_namespace
|
i.created_at, i.updated_at, i.web_url, p.path_with_namespace
|
||||||
@@ -333,20 +336,22 @@ struct MrRow {
|
|||||||
/// Find MR by iid, optionally filtered by project.
|
/// Find MR by iid, optionally filtered by project.
|
||||||
fn find_mr(conn: &Connection, iid: i64, project_filter: Option<&str>) -> Result<MrRow> {
|
fn find_mr(conn: &Connection, iid: i64, project_filter: Option<&str>) -> Result<MrRow> {
|
||||||
let (sql, params): (&str, Vec<Box<dyn rusqlite::ToSql>>) = match project_filter {
|
let (sql, params): (&str, Vec<Box<dyn rusqlite::ToSql>>) = match project_filter {
|
||||||
Some(project) => (
|
Some(project) => {
|
||||||
|
let project_id = resolve_project(conn, project)?;
|
||||||
|
(
|
||||||
"SELECT m.id, m.iid, m.title, m.description, m.state, m.draft,
|
"SELECT m.id, m.iid, m.title, m.description, m.state, m.draft,
|
||||||
m.author_username, m.source_branch, m.target_branch,
|
m.author_username, m.source_branch, m.target_branch,
|
||||||
m.created_at, m.updated_at, m.merged_at, m.closed_at,
|
m.created_at, m.updated_at, m.merged_at, m.closed_at,
|
||||||
m.web_url, p.path_with_namespace
|
m.web_url, p.path_with_namespace
|
||||||
FROM merge_requests m
|
FROM merge_requests m
|
||||||
JOIN projects p ON m.project_id = p.id
|
JOIN projects p ON m.project_id = p.id
|
||||||
WHERE m.iid = ? AND (p.path_with_namespace = ? OR p.path_with_namespace LIKE ?)",
|
WHERE m.iid = ? AND m.project_id = ?",
|
||||||
vec![
|
vec![
|
||||||
Box::new(iid),
|
Box::new(iid),
|
||||||
Box::new(project.to_string()),
|
Box::new(project_id),
|
||||||
Box::new(format!("%/{}", project)),
|
|
||||||
],
|
],
|
||||||
),
|
)
|
||||||
|
}
|
||||||
None => (
|
None => (
|
||||||
"SELECT m.id, m.iid, m.title, m.description, m.state, m.draft,
|
"SELECT m.id, m.iid, m.title, m.description, m.state, m.draft,
|
||||||
m.author_username, m.source_branch, m.target_branch,
|
m.author_username, m.source_branch, m.target_branch,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use crate::core::error::Result;
|
|||||||
|
|
||||||
use super::embed::run_embed;
|
use super::embed::run_embed;
|
||||||
use super::generate_docs::run_generate_docs;
|
use super::generate_docs::run_generate_docs;
|
||||||
use super::ingest::run_ingest;
|
use super::ingest::{IngestDisplay, run_ingest};
|
||||||
|
|
||||||
/// Options for the sync command.
|
/// Options for the sync command.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
@@ -18,6 +18,7 @@ pub struct SyncOptions {
|
|||||||
pub force: bool,
|
pub force: bool,
|
||||||
pub no_embed: bool,
|
pub no_embed: bool,
|
||||||
pub no_docs: bool,
|
pub no_docs: bool,
|
||||||
|
pub robot_mode: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Result of the sync command.
|
/// Result of the sync command.
|
||||||
@@ -34,15 +35,21 @@ pub struct SyncResult {
|
|||||||
pub async fn run_sync(config: &Config, options: SyncOptions) -> Result<SyncResult> {
|
pub async fn run_sync(config: &Config, options: SyncOptions) -> Result<SyncResult> {
|
||||||
let mut result = SyncResult::default();
|
let mut result = SyncResult::default();
|
||||||
|
|
||||||
|
let ingest_display = if options.robot_mode {
|
||||||
|
IngestDisplay::silent()
|
||||||
|
} else {
|
||||||
|
IngestDisplay::progress_only()
|
||||||
|
};
|
||||||
|
|
||||||
// Stage 1: Ingest issues
|
// Stage 1: Ingest issues
|
||||||
info!("Sync stage 1/4: ingesting issues");
|
info!("Sync stage 1/4: ingesting issues");
|
||||||
let issues_result = run_ingest(config, "issues", None, options.force, options.full, true).await?;
|
let issues_result = run_ingest(config, "issues", None, options.force, options.full, ingest_display).await?;
|
||||||
result.issues_updated = issues_result.issues_upserted;
|
result.issues_updated = issues_result.issues_upserted;
|
||||||
result.discussions_fetched += issues_result.discussions_fetched;
|
result.discussions_fetched += issues_result.discussions_fetched;
|
||||||
|
|
||||||
// Stage 2: Ingest MRs
|
// Stage 2: Ingest MRs
|
||||||
info!("Sync stage 2/4: ingesting merge requests");
|
info!("Sync stage 2/4: ingesting merge requests");
|
||||||
let mrs_result = run_ingest(config, "mrs", None, options.force, options.full, true).await?;
|
let mrs_result = run_ingest(config, "mrs", None, options.force, options.full, ingest_display).await?;
|
||||||
result.mrs_updated = mrs_result.mrs_upserted;
|
result.mrs_updated = mrs_result.mrs_upserted;
|
||||||
result.discussions_fetched += mrs_result.discussions_fetched;
|
result.discussions_fetched += mrs_result.discussions_fetched;
|
||||||
|
|
||||||
|
|||||||
148
src/cli/mod.rs
148
src/cli/mod.rs
@@ -22,6 +22,14 @@ pub struct Cli {
|
|||||||
#[arg(short = 'J', long = "json", global = true)]
|
#[arg(short = 'J', long = "json", global = true)]
|
||||||
pub json: bool,
|
pub json: bool,
|
||||||
|
|
||||||
|
/// Color output: auto (default), always, or never
|
||||||
|
#[arg(long, global = true, value_parser = ["auto", "always", "never"], default_value = "auto")]
|
||||||
|
pub color: String,
|
||||||
|
|
||||||
|
/// Suppress non-essential output
|
||||||
|
#[arg(short = 'q', long, global = true)]
|
||||||
|
pub quiet: bool,
|
||||||
|
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: Commands,
|
pub command: Commands,
|
||||||
}
|
}
|
||||||
@@ -84,9 +92,11 @@ pub enum Commands {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/// Create timestamped database backup
|
/// Create timestamped database backup
|
||||||
|
#[command(hide = true)]
|
||||||
Backup,
|
Backup,
|
||||||
|
|
||||||
/// Delete database and reset all state
|
/// Delete database and reset all state
|
||||||
|
#[command(hide = true)]
|
||||||
Reset {
|
Reset {
|
||||||
/// Skip confirmation prompt
|
/// Skip confirmation prompt
|
||||||
#[arg(short = 'y', long)]
|
#[arg(short = 'y', long)]
|
||||||
@@ -119,6 +129,14 @@ pub enum Commands {
|
|||||||
#[command(name = "robot-docs")]
|
#[command(name = "robot-docs")]
|
||||||
RobotDocs,
|
RobotDocs,
|
||||||
|
|
||||||
|
/// Generate shell completions
|
||||||
|
#[command(hide = true)]
|
||||||
|
Completions {
|
||||||
|
/// Shell to generate completions for
|
||||||
|
#[arg(value_parser = ["bash", "zsh", "fish", "powershell"])]
|
||||||
|
shell: String,
|
||||||
|
},
|
||||||
|
|
||||||
// --- Hidden backward-compat aliases ---
|
// --- Hidden backward-compat aliases ---
|
||||||
/// List issues or MRs (deprecated: use 'lore issues' or 'lore mrs')
|
/// List issues or MRs (deprecated: use 'lore issues' or 'lore mrs')
|
||||||
#[command(hide = true)]
|
#[command(hide = true)]
|
||||||
@@ -195,56 +213,65 @@ pub struct IssuesArgs {
|
|||||||
pub iid: Option<i64>,
|
pub iid: Option<i64>,
|
||||||
|
|
||||||
/// Maximum results
|
/// Maximum results
|
||||||
#[arg(short = 'n', long = "limit", default_value = "50")]
|
#[arg(short = 'n', long = "limit", default_value = "50", help_heading = "Output")]
|
||||||
pub limit: usize,
|
pub limit: usize,
|
||||||
|
|
||||||
/// Filter by state (opened, closed, all)
|
/// Filter by state (opened, closed, all)
|
||||||
#[arg(short = 's', long)]
|
#[arg(short = 's', long, help_heading = "Filters")]
|
||||||
pub state: Option<String>,
|
pub state: Option<String>,
|
||||||
|
|
||||||
/// Filter by project path
|
/// Filter by project path
|
||||||
#[arg(short = 'p', long)]
|
#[arg(short = 'p', long, help_heading = "Filters")]
|
||||||
pub project: Option<String>,
|
pub project: Option<String>,
|
||||||
|
|
||||||
/// Filter by author username
|
/// Filter by author username
|
||||||
#[arg(short = 'a', long)]
|
#[arg(short = 'a', long, help_heading = "Filters")]
|
||||||
pub author: Option<String>,
|
pub author: Option<String>,
|
||||||
|
|
||||||
/// Filter by assignee username
|
/// Filter by assignee username
|
||||||
#[arg(short = 'A', long)]
|
#[arg(short = 'A', long, help_heading = "Filters")]
|
||||||
pub assignee: Option<String>,
|
pub assignee: Option<String>,
|
||||||
|
|
||||||
/// Filter by label (repeatable, AND logic)
|
/// Filter by label (repeatable, AND logic)
|
||||||
#[arg(short = 'l', long)]
|
#[arg(short = 'l', long, help_heading = "Filters")]
|
||||||
pub label: Option<Vec<String>>,
|
pub label: Option<Vec<String>>,
|
||||||
|
|
||||||
/// Filter by milestone title
|
/// Filter by milestone title
|
||||||
#[arg(short = 'm', long)]
|
#[arg(short = 'm', long, help_heading = "Filters")]
|
||||||
pub milestone: Option<String>,
|
pub milestone: Option<String>,
|
||||||
|
|
||||||
/// Filter by time (7d, 2w, 1m, or YYYY-MM-DD)
|
/// Filter by time (7d, 2w, 1m, or YYYY-MM-DD)
|
||||||
#[arg(long)]
|
#[arg(long, help_heading = "Filters")]
|
||||||
pub since: Option<String>,
|
pub since: Option<String>,
|
||||||
|
|
||||||
/// Filter by due date (before this date, YYYY-MM-DD)
|
/// Filter by due date (before this date, YYYY-MM-DD)
|
||||||
#[arg(long = "due-before")]
|
#[arg(long = "due-before", help_heading = "Filters")]
|
||||||
pub due_before: Option<String>,
|
pub due_before: Option<String>,
|
||||||
|
|
||||||
/// Show only issues with a due date
|
/// Show only issues with a due date
|
||||||
#[arg(long = "has-due")]
|
#[arg(long = "has-due", help_heading = "Filters", overrides_with = "no_has_due")]
|
||||||
pub has_due: bool,
|
pub has_due: bool,
|
||||||
|
|
||||||
|
#[arg(long = "no-has-due", hide = true, overrides_with = "has_due")]
|
||||||
|
pub no_has_due: bool,
|
||||||
|
|
||||||
/// Sort field (updated, created, iid)
|
/// Sort field (updated, created, iid)
|
||||||
#[arg(long, value_parser = ["updated", "created", "iid"], default_value = "updated")]
|
#[arg(long, value_parser = ["updated", "created", "iid"], default_value = "updated", help_heading = "Sorting")]
|
||||||
pub sort: String,
|
pub sort: String,
|
||||||
|
|
||||||
/// Sort ascending (default: descending)
|
/// Sort ascending (default: descending)
|
||||||
#[arg(long)]
|
#[arg(long, help_heading = "Sorting", overrides_with = "no_asc")]
|
||||||
pub asc: bool,
|
pub asc: bool,
|
||||||
|
|
||||||
|
#[arg(long = "no-asc", hide = true, overrides_with = "asc")]
|
||||||
|
pub no_asc: bool,
|
||||||
|
|
||||||
/// Open first matching item in browser
|
/// Open first matching item in browser
|
||||||
#[arg(short = 'o', long)]
|
#[arg(short = 'o', long, help_heading = "Actions", overrides_with = "no_open")]
|
||||||
pub open: bool,
|
pub open: bool,
|
||||||
|
|
||||||
|
#[arg(long = "no-open", hide = true, overrides_with = "open")]
|
||||||
|
pub no_open: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Arguments for `lore mrs [IID]`
|
/// Arguments for `lore mrs [IID]`
|
||||||
@@ -254,64 +281,70 @@ pub struct MrsArgs {
|
|||||||
pub iid: Option<i64>,
|
pub iid: Option<i64>,
|
||||||
|
|
||||||
/// Maximum results
|
/// Maximum results
|
||||||
#[arg(short = 'n', long = "limit", default_value = "50")]
|
#[arg(short = 'n', long = "limit", default_value = "50", help_heading = "Output")]
|
||||||
pub limit: usize,
|
pub limit: usize,
|
||||||
|
|
||||||
/// Filter by state (opened, merged, closed, locked, all)
|
/// Filter by state (opened, merged, closed, locked, all)
|
||||||
#[arg(short = 's', long)]
|
#[arg(short = 's', long, help_heading = "Filters")]
|
||||||
pub state: Option<String>,
|
pub state: Option<String>,
|
||||||
|
|
||||||
/// Filter by project path
|
/// Filter by project path
|
||||||
#[arg(short = 'p', long)]
|
#[arg(short = 'p', long, help_heading = "Filters")]
|
||||||
pub project: Option<String>,
|
pub project: Option<String>,
|
||||||
|
|
||||||
/// Filter by author username
|
/// Filter by author username
|
||||||
#[arg(short = 'a', long)]
|
#[arg(short = 'a', long, help_heading = "Filters")]
|
||||||
pub author: Option<String>,
|
pub author: Option<String>,
|
||||||
|
|
||||||
/// Filter by assignee username
|
/// Filter by assignee username
|
||||||
#[arg(short = 'A', long)]
|
#[arg(short = 'A', long, help_heading = "Filters")]
|
||||||
pub assignee: Option<String>,
|
pub assignee: Option<String>,
|
||||||
|
|
||||||
/// Filter by reviewer username
|
/// Filter by reviewer username
|
||||||
#[arg(short = 'r', long)]
|
#[arg(short = 'r', long, help_heading = "Filters")]
|
||||||
pub reviewer: Option<String>,
|
pub reviewer: Option<String>,
|
||||||
|
|
||||||
/// Filter by label (repeatable, AND logic)
|
/// Filter by label (repeatable, AND logic)
|
||||||
#[arg(short = 'l', long)]
|
#[arg(short = 'l', long, help_heading = "Filters")]
|
||||||
pub label: Option<Vec<String>>,
|
pub label: Option<Vec<String>>,
|
||||||
|
|
||||||
/// Filter by time (7d, 2w, 1m, or YYYY-MM-DD)
|
/// Filter by time (7d, 2w, 1m, or YYYY-MM-DD)
|
||||||
#[arg(long)]
|
#[arg(long, help_heading = "Filters")]
|
||||||
pub since: Option<String>,
|
pub since: Option<String>,
|
||||||
|
|
||||||
/// Show only draft MRs
|
/// Show only draft MRs
|
||||||
#[arg(short = 'd', long, conflicts_with = "no_draft")]
|
#[arg(short = 'd', long, conflicts_with = "no_draft", help_heading = "Filters")]
|
||||||
pub draft: bool,
|
pub draft: bool,
|
||||||
|
|
||||||
/// Exclude draft MRs
|
/// Exclude draft MRs
|
||||||
#[arg(short = 'D', long = "no-draft", conflicts_with = "draft")]
|
#[arg(short = 'D', long = "no-draft", conflicts_with = "draft", help_heading = "Filters")]
|
||||||
pub no_draft: bool,
|
pub no_draft: bool,
|
||||||
|
|
||||||
/// Filter by target branch
|
/// Filter by target branch
|
||||||
#[arg(long)]
|
#[arg(long, help_heading = "Filters")]
|
||||||
pub target: Option<String>,
|
pub target: Option<String>,
|
||||||
|
|
||||||
/// Filter by source branch
|
/// Filter by source branch
|
||||||
#[arg(long)]
|
#[arg(long, help_heading = "Filters")]
|
||||||
pub source: Option<String>,
|
pub source: Option<String>,
|
||||||
|
|
||||||
/// Sort field (updated, created, iid)
|
/// Sort field (updated, created, iid)
|
||||||
#[arg(long, value_parser = ["updated", "created", "iid"], default_value = "updated")]
|
#[arg(long, value_parser = ["updated", "created", "iid"], default_value = "updated", help_heading = "Sorting")]
|
||||||
pub sort: String,
|
pub sort: String,
|
||||||
|
|
||||||
/// Sort ascending (default: descending)
|
/// Sort ascending (default: descending)
|
||||||
#[arg(long)]
|
#[arg(long, help_heading = "Sorting", overrides_with = "no_asc")]
|
||||||
pub asc: bool,
|
pub asc: bool,
|
||||||
|
|
||||||
|
#[arg(long = "no-asc", hide = true, overrides_with = "asc")]
|
||||||
|
pub no_asc: bool,
|
||||||
|
|
||||||
/// Open first matching item in browser
|
/// Open first matching item in browser
|
||||||
#[arg(short = 'o', long)]
|
#[arg(short = 'o', long, help_heading = "Actions", overrides_with = "no_open")]
|
||||||
pub open: bool,
|
pub open: bool,
|
||||||
|
|
||||||
|
#[arg(long = "no-open", hide = true, overrides_with = "open")]
|
||||||
|
pub no_open: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Arguments for `lore ingest [ENTITY]`
|
/// Arguments for `lore ingest [ENTITY]`
|
||||||
@@ -326,23 +359,32 @@ pub struct IngestArgs {
|
|||||||
pub project: Option<String>,
|
pub project: Option<String>,
|
||||||
|
|
||||||
/// Override stale sync lock
|
/// Override stale sync lock
|
||||||
#[arg(short = 'f', long)]
|
#[arg(short = 'f', long, overrides_with = "no_force")]
|
||||||
pub force: bool,
|
pub force: bool,
|
||||||
|
|
||||||
|
#[arg(long = "no-force", hide = true, overrides_with = "force")]
|
||||||
|
pub no_force: bool,
|
||||||
|
|
||||||
/// Full re-sync: reset cursors and fetch all data from scratch
|
/// Full re-sync: reset cursors and fetch all data from scratch
|
||||||
#[arg(long)]
|
#[arg(long, overrides_with = "no_full")]
|
||||||
pub full: bool,
|
pub full: bool,
|
||||||
|
|
||||||
|
#[arg(long = "no-full", hide = true, overrides_with = "full")]
|
||||||
|
pub no_full: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Arguments for `lore stats`
|
/// Arguments for `lore stats`
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
pub struct StatsArgs {
|
pub struct StatsArgs {
|
||||||
/// Run integrity checks
|
/// Run integrity checks
|
||||||
#[arg(long)]
|
#[arg(long, overrides_with = "no_check")]
|
||||||
pub check: bool,
|
pub check: bool,
|
||||||
|
|
||||||
/// Repair integrity issues (requires --check)
|
#[arg(long = "no-check", hide = true, overrides_with = "check")]
|
||||||
#[arg(long, requires = "check")]
|
pub no_check: bool,
|
||||||
|
|
||||||
|
/// Repair integrity issues (auto-enables --check)
|
||||||
|
#[arg(long)]
|
||||||
pub repair: bool,
|
pub repair: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,47 +395,50 @@ pub struct SearchArgs {
|
|||||||
pub query: String,
|
pub query: String,
|
||||||
|
|
||||||
/// Search mode (lexical, hybrid, semantic)
|
/// Search mode (lexical, hybrid, semantic)
|
||||||
#[arg(long, default_value = "hybrid")]
|
#[arg(long, default_value = "hybrid", value_parser = ["lexical", "hybrid", "semantic"], help_heading = "Output")]
|
||||||
pub mode: String,
|
pub mode: String,
|
||||||
|
|
||||||
/// Filter by source type (issue, mr, discussion)
|
/// Filter by source type (issue, mr, discussion)
|
||||||
#[arg(long = "type", value_name = "TYPE")]
|
#[arg(long = "type", value_name = "TYPE", value_parser = ["issue", "mr", "discussion"], help_heading = "Filters")]
|
||||||
pub source_type: Option<String>,
|
pub source_type: Option<String>,
|
||||||
|
|
||||||
/// Filter by author username
|
/// Filter by author username
|
||||||
#[arg(long)]
|
#[arg(long, help_heading = "Filters")]
|
||||||
pub author: Option<String>,
|
pub author: Option<String>,
|
||||||
|
|
||||||
/// Filter by project path
|
/// Filter by project path
|
||||||
#[arg(short = 'p', long)]
|
#[arg(short = 'p', long, help_heading = "Filters")]
|
||||||
pub project: Option<String>,
|
pub project: Option<String>,
|
||||||
|
|
||||||
/// Filter by label (repeatable, AND logic)
|
/// Filter by label (repeatable, AND logic)
|
||||||
#[arg(long, action = clap::ArgAction::Append)]
|
#[arg(long, action = clap::ArgAction::Append, help_heading = "Filters")]
|
||||||
pub label: Vec<String>,
|
pub label: Vec<String>,
|
||||||
|
|
||||||
/// Filter by file path (trailing / for prefix match)
|
/// Filter by file path (trailing / for prefix match)
|
||||||
#[arg(long)]
|
#[arg(long, help_heading = "Filters")]
|
||||||
pub path: Option<String>,
|
pub path: Option<String>,
|
||||||
|
|
||||||
/// Filter by created after (7d, 2w, or YYYY-MM-DD)
|
/// Filter by created after (7d, 2w, or YYYY-MM-DD)
|
||||||
#[arg(long)]
|
#[arg(long, help_heading = "Filters")]
|
||||||
pub after: Option<String>,
|
pub after: Option<String>,
|
||||||
|
|
||||||
/// Filter by updated after (7d, 2w, or YYYY-MM-DD)
|
/// Filter by updated after (7d, 2w, or YYYY-MM-DD)
|
||||||
#[arg(long = "updated-after")]
|
#[arg(long = "updated-after", help_heading = "Filters")]
|
||||||
pub updated_after: Option<String>,
|
pub updated_after: Option<String>,
|
||||||
|
|
||||||
/// Maximum results (default 20, max 100)
|
/// Maximum results (default 20, max 100)
|
||||||
#[arg(short = 'n', long = "limit", default_value = "20")]
|
#[arg(short = 'n', long = "limit", default_value = "20", help_heading = "Output")]
|
||||||
pub limit: usize,
|
pub limit: usize,
|
||||||
|
|
||||||
/// Show ranking explanation per result
|
/// Show ranking explanation per result
|
||||||
#[arg(long)]
|
#[arg(long, help_heading = "Output", overrides_with = "no_explain")]
|
||||||
pub explain: bool,
|
pub explain: bool,
|
||||||
|
|
||||||
|
#[arg(long = "no-explain", hide = true, overrides_with = "explain")]
|
||||||
|
pub no_explain: bool,
|
||||||
|
|
||||||
/// FTS query mode: safe (default) or raw
|
/// FTS query mode: safe (default) or raw
|
||||||
#[arg(long = "fts-mode", default_value = "safe")]
|
#[arg(long = "fts-mode", default_value = "safe", value_parser = ["safe", "raw"], help_heading = "Output")]
|
||||||
pub fts_mode: String,
|
pub fts_mode: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,13 +458,19 @@ pub struct GenerateDocsArgs {
|
|||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
pub struct SyncArgs {
|
pub struct SyncArgs {
|
||||||
/// Reset cursors, fetch everything
|
/// Reset cursors, fetch everything
|
||||||
#[arg(long)]
|
#[arg(long, overrides_with = "no_full")]
|
||||||
pub full: bool,
|
pub full: bool,
|
||||||
|
|
||||||
|
#[arg(long = "no-full", hide = true, overrides_with = "full")]
|
||||||
|
pub no_full: bool,
|
||||||
|
|
||||||
/// Override stale lock
|
/// Override stale lock
|
||||||
#[arg(long)]
|
#[arg(long, overrides_with = "no_force")]
|
||||||
pub force: bool,
|
pub force: bool,
|
||||||
|
|
||||||
|
#[arg(long = "no-force", hide = true, overrides_with = "force")]
|
||||||
|
pub no_force: bool,
|
||||||
|
|
||||||
/// Skip embedding step
|
/// Skip embedding step
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub no_embed: bool,
|
pub no_embed: bool,
|
||||||
@@ -433,8 +484,11 @@ pub struct SyncArgs {
|
|||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
pub struct EmbedArgs {
|
pub struct EmbedArgs {
|
||||||
/// Retry previously failed embeddings
|
/// Retry previously failed embeddings
|
||||||
#[arg(long)]
|
#[arg(long, overrides_with = "no_retry_failed")]
|
||||||
pub retry_failed: bool,
|
pub retry_failed: bool,
|
||||||
|
|
||||||
|
#[arg(long = "no-retry-failed", hide = true, overrides_with = "retry_failed")]
|
||||||
|
pub no_retry_failed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Arguments for `lore count <ENTITY>`
|
/// Arguments for `lore count <ENTITY>`
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ pub enum ErrorCode {
|
|||||||
OllamaUnavailable,
|
OllamaUnavailable,
|
||||||
OllamaModelNotFound,
|
OllamaModelNotFound,
|
||||||
EmbeddingFailed,
|
EmbeddingFailed,
|
||||||
|
NotFound,
|
||||||
|
Ambiguous,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for ErrorCode {
|
impl std::fmt::Display for ErrorCode {
|
||||||
@@ -45,6 +47,8 @@ impl std::fmt::Display for ErrorCode {
|
|||||||
Self::OllamaUnavailable => "OLLAMA_UNAVAILABLE",
|
Self::OllamaUnavailable => "OLLAMA_UNAVAILABLE",
|
||||||
Self::OllamaModelNotFound => "OLLAMA_MODEL_NOT_FOUND",
|
Self::OllamaModelNotFound => "OLLAMA_MODEL_NOT_FOUND",
|
||||||
Self::EmbeddingFailed => "EMBEDDING_FAILED",
|
Self::EmbeddingFailed => "EMBEDDING_FAILED",
|
||||||
|
Self::NotFound => "NOT_FOUND",
|
||||||
|
Self::Ambiguous => "AMBIGUOUS",
|
||||||
};
|
};
|
||||||
write!(f, "{code}")
|
write!(f, "{code}")
|
||||||
}
|
}
|
||||||
@@ -55,7 +59,7 @@ impl ErrorCode {
|
|||||||
pub fn exit_code(&self) -> i32 {
|
pub fn exit_code(&self) -> i32 {
|
||||||
match self {
|
match self {
|
||||||
Self::InternalError => 1,
|
Self::InternalError => 1,
|
||||||
Self::ConfigNotFound => 2,
|
Self::ConfigNotFound => 20,
|
||||||
Self::ConfigInvalid => 3,
|
Self::ConfigInvalid => 3,
|
||||||
Self::TokenNotSet => 4,
|
Self::TokenNotSet => 4,
|
||||||
Self::GitLabAuthFailed => 5,
|
Self::GitLabAuthFailed => 5,
|
||||||
@@ -70,6 +74,8 @@ impl ErrorCode {
|
|||||||
Self::OllamaUnavailable => 14,
|
Self::OllamaUnavailable => 14,
|
||||||
Self::OllamaModelNotFound => 15,
|
Self::OllamaModelNotFound => 15,
|
||||||
Self::EmbeddingFailed => 16,
|
Self::EmbeddingFailed => 16,
|
||||||
|
Self::NotFound => 17,
|
||||||
|
Self::Ambiguous => 18,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,8 +180,8 @@ impl LoreError {
|
|||||||
Self::Json(_) => ErrorCode::InternalError,
|
Self::Json(_) => ErrorCode::InternalError,
|
||||||
Self::Io(_) => ErrorCode::IoError,
|
Self::Io(_) => ErrorCode::IoError,
|
||||||
Self::Transform(_) => ErrorCode::TransformError,
|
Self::Transform(_) => ErrorCode::TransformError,
|
||||||
Self::NotFound(_) => ErrorCode::GitLabNotFound,
|
Self::NotFound(_) => ErrorCode::NotFound,
|
||||||
Self::Ambiguous(_) => ErrorCode::GitLabNotFound,
|
Self::Ambiguous(_) => ErrorCode::Ambiguous,
|
||||||
Self::Other(_) => ErrorCode::InternalError,
|
Self::Other(_) => ErrorCode::InternalError,
|
||||||
Self::OllamaUnavailable { .. } => ErrorCode::OllamaUnavailable,
|
Self::OllamaUnavailable { .. } => ErrorCode::OllamaUnavailable,
|
||||||
Self::OllamaModelNotFound { .. } => ErrorCode::OllamaModelNotFound,
|
Self::OllamaModelNotFound { .. } => ErrorCode::OllamaModelNotFound,
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ use super::error::{LoreError, Result};
|
|||||||
/// Resolve a project string to a project_id using cascading match:
|
/// Resolve a project string to a project_id using cascading match:
|
||||||
/// 1. Exact match on path_with_namespace
|
/// 1. Exact match on path_with_namespace
|
||||||
/// 2. Case-insensitive exact match
|
/// 2. Case-insensitive exact match
|
||||||
/// 3. Suffix match (only if unambiguous)
|
/// 3. Suffix match (e.g., "auth-service" matches "group/auth-service") — only if unambiguous
|
||||||
/// 4. Error with available projects list
|
/// 4. Substring match (e.g., "typescript" matches "vs/typescript-code") — only if unambiguous
|
||||||
|
/// 5. Error with available projects list
|
||||||
pub fn resolve_project(conn: &Connection, project_str: &str) -> Result<i64> {
|
pub fn resolve_project(conn: &Connection, project_str: &str) -> Result<i64> {
|
||||||
// Step 1: Exact match
|
// Step 1: Exact match
|
||||||
let exact = conn.query_row(
|
let exact = conn.query_row(
|
||||||
@@ -44,7 +45,7 @@ pub fn resolve_project(conn: &Connection, project_str: &str) -> Result<i64> {
|
|||||||
1 => return Ok(suffix_matches[0].0),
|
1 => return Ok(suffix_matches[0].0),
|
||||||
n if n > 1 => {
|
n if n > 1 => {
|
||||||
let matching: Vec<String> = suffix_matches.iter().map(|(_, p)| p.clone()).collect();
|
let matching: Vec<String> = suffix_matches.iter().map(|(_, p)| p.clone()).collect();
|
||||||
return Err(LoreError::Other(format!(
|
return Err(LoreError::Ambiguous(format!(
|
||||||
"Project '{}' is ambiguous. Matching projects:\n{}\n\nHint: Use the full path, e.g., --project={}",
|
"Project '{}' is ambiguous. Matching projects:\n{}\n\nHint: Use the full path, e.g., --project={}",
|
||||||
project_str,
|
project_str,
|
||||||
matching.iter().map(|p| format!(" {}", p)).collect::<Vec<_>>().join("\n"),
|
matching.iter().map(|p| format!(" {}", p)).collect::<Vec<_>>().join("\n"),
|
||||||
@@ -54,7 +55,32 @@ pub fn resolve_project(conn: &Connection, project_str: &str) -> Result<i64> {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: No match — list available projects
|
// Step 4: Case-insensitive substring match (unambiguous)
|
||||||
|
let mut substr_stmt = conn.prepare(
|
||||||
|
"SELECT id, path_with_namespace FROM projects
|
||||||
|
WHERE LOWER(path_with_namespace) LIKE '%' || LOWER(?1) || '%'"
|
||||||
|
)?;
|
||||||
|
let substr_matches: Vec<(i64, String)> = substr_stmt
|
||||||
|
.query_map(rusqlite::params![project_str], |row| {
|
||||||
|
Ok((row.get(0)?, row.get(1)?))
|
||||||
|
})?
|
||||||
|
.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
|
match substr_matches.len() {
|
||||||
|
1 => return Ok(substr_matches[0].0),
|
||||||
|
n if n > 1 => {
|
||||||
|
let matching: Vec<String> = substr_matches.iter().map(|(_, p)| p.clone()).collect();
|
||||||
|
return Err(LoreError::Ambiguous(format!(
|
||||||
|
"Project '{}' is ambiguous. Matching projects:\n{}\n\nHint: Use the full path, e.g., --project={}",
|
||||||
|
project_str,
|
||||||
|
matching.iter().map(|p| format!(" {}", p)).collect::<Vec<_>>().join("\n"),
|
||||||
|
matching[0]
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5: No match — list available projects
|
||||||
let mut all_stmt = conn.prepare(
|
let mut all_stmt = conn.prepare(
|
||||||
"SELECT path_with_namespace FROM projects ORDER BY path_with_namespace"
|
"SELECT path_with_namespace FROM projects ORDER BY path_with_namespace"
|
||||||
)?;
|
)?;
|
||||||
@@ -143,6 +169,48 @@ mod tests {
|
|||||||
assert!(msg.contains("frontend/auth-service"));
|
assert!(msg.contains("frontend/auth-service"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_substring_unambiguous() {
|
||||||
|
let conn = setup_db();
|
||||||
|
insert_project(&conn, 1, "vs/python-code");
|
||||||
|
insert_project(&conn, 2, "vs/typescript-code");
|
||||||
|
let id = resolve_project(&conn, "typescript").unwrap();
|
||||||
|
assert_eq!(id, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_substring_case_insensitive() {
|
||||||
|
let conn = setup_db();
|
||||||
|
insert_project(&conn, 1, "vs/python-code");
|
||||||
|
insert_project(&conn, 2, "vs/typescript-code");
|
||||||
|
let id = resolve_project(&conn, "TypeScript").unwrap();
|
||||||
|
assert_eq!(id, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_substring_ambiguous() {
|
||||||
|
let conn = setup_db();
|
||||||
|
insert_project(&conn, 1, "vs/python-code");
|
||||||
|
insert_project(&conn, 2, "vs/typescript-code");
|
||||||
|
// "code" matches both projects
|
||||||
|
let err = resolve_project(&conn, "code").unwrap_err();
|
||||||
|
let msg = err.to_string();
|
||||||
|
assert!(msg.contains("ambiguous"), "Expected ambiguous error, got: {}", msg);
|
||||||
|
assert!(msg.contains("vs/python-code"));
|
||||||
|
assert!(msg.contains("vs/typescript-code"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_suffix_preferred_over_substring() {
|
||||||
|
// Suffix match (step 3) should resolve before substring (step 4)
|
||||||
|
let conn = setup_db();
|
||||||
|
insert_project(&conn, 1, "backend/auth-service");
|
||||||
|
insert_project(&conn, 2, "backend/auth-service-v2");
|
||||||
|
// "auth-service" is an exact suffix of project 1
|
||||||
|
let id = resolve_project(&conn, "auth-service").unwrap();
|
||||||
|
assert_eq!(id, 1);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_no_match() {
|
fn test_no_match() {
|
||||||
let conn = setup_db();
|
let conn = setup_db();
|
||||||
|
|||||||
171
src/main.rs
171
src/main.rs
@@ -20,6 +20,7 @@ use lore::cli::commands::{
|
|||||||
print_show_mr_json, print_sync_status, print_sync_status_json, run_auth_test, run_count,
|
print_show_mr_json, print_sync_status, print_sync_status_json, run_auth_test, run_count,
|
||||||
run_doctor, run_embed, run_generate_docs, run_ingest, run_init, run_list_issues, run_list_mrs,
|
run_doctor, run_embed, run_generate_docs, run_ingest, run_init, run_list_issues, run_list_mrs,
|
||||||
run_search, run_show_issue, run_show_mr, run_stats, run_sync, run_sync_status, SyncOptions,
|
run_search, run_show_issue, run_show_mr, run_stats, run_sync, run_sync_status, SyncOptions,
|
||||||
|
IngestDisplay,
|
||||||
};
|
};
|
||||||
use lore::cli::{
|
use lore::cli::{
|
||||||
Cli, Commands, CountArgs, EmbedArgs, GenerateDocsArgs, IngestArgs, IssuesArgs, MrsArgs,
|
Cli, Commands, CountArgs, EmbedArgs, GenerateDocsArgs, IngestArgs, IssuesArgs, MrsArgs,
|
||||||
@@ -32,6 +33,12 @@ use lore::core::paths::get_db_path;
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
// Reset SIGPIPE to default behavior so piping (e.g. `lore issues | head`) doesn't panic
|
||||||
|
#[cfg(unix)]
|
||||||
|
unsafe {
|
||||||
|
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize logging with indicatif support for clean progress bar output
|
// Initialize logging with indicatif support for clean progress bar output
|
||||||
let indicatif_layer = tracing_indicatif::IndicatifLayer::new();
|
let indicatif_layer = tracing_indicatif::IndicatifLayer::new();
|
||||||
|
|
||||||
@@ -52,6 +59,16 @@ async fn main() {
|
|||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
let robot_mode = cli.is_robot_mode();
|
let robot_mode = cli.is_robot_mode();
|
||||||
|
|
||||||
|
// Apply color settings (console crate handles NO_COLOR/CLICOLOR natively in "auto" mode)
|
||||||
|
match cli.color.as_str() {
|
||||||
|
"never" => console::set_colors_enabled(false),
|
||||||
|
"always" => console::set_colors_enabled(true),
|
||||||
|
"auto" => {} // console crate handles this natively
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
let quiet = cli.quiet;
|
||||||
|
|
||||||
let result = match cli.command {
|
let result = match cli.command {
|
||||||
Commands::Issues(args) => handle_issues(cli.config.as_deref(), args, robot_mode).await,
|
Commands::Issues(args) => handle_issues(cli.config.as_deref(), args, robot_mode).await,
|
||||||
Commands::Mrs(args) => handle_mrs(cli.config.as_deref(), args, robot_mode).await,
|
Commands::Mrs(args) => handle_mrs(cli.config.as_deref(), args, robot_mode).await,
|
||||||
@@ -59,7 +76,7 @@ async fn main() {
|
|||||||
Commands::Stats(args) => handle_stats(cli.config.as_deref(), args, robot_mode).await,
|
Commands::Stats(args) => handle_stats(cli.config.as_deref(), args, robot_mode).await,
|
||||||
Commands::Embed(args) => handle_embed(cli.config.as_deref(), args, robot_mode).await,
|
Commands::Embed(args) => handle_embed(cli.config.as_deref(), args, robot_mode).await,
|
||||||
Commands::Sync(args) => handle_sync_cmd(cli.config.as_deref(), args, robot_mode).await,
|
Commands::Sync(args) => handle_sync_cmd(cli.config.as_deref(), args, robot_mode).await,
|
||||||
Commands::Ingest(args) => handle_ingest(cli.config.as_deref(), args, robot_mode).await,
|
Commands::Ingest(args) => handle_ingest(cli.config.as_deref(), args, robot_mode, quiet).await,
|
||||||
Commands::Count(args) => {
|
Commands::Count(args) => {
|
||||||
handle_count(cli.config.as_deref(), args, robot_mode).await
|
handle_count(cli.config.as_deref(), args, robot_mode).await
|
||||||
}
|
}
|
||||||
@@ -67,6 +84,7 @@ async fn main() {
|
|||||||
Commands::Auth => handle_auth_test(cli.config.as_deref(), robot_mode).await,
|
Commands::Auth => handle_auth_test(cli.config.as_deref(), robot_mode).await,
|
||||||
Commands::Doctor => handle_doctor(cli.config.as_deref(), robot_mode).await,
|
Commands::Doctor => handle_doctor(cli.config.as_deref(), robot_mode).await,
|
||||||
Commands::Version => handle_version(robot_mode),
|
Commands::Version => handle_version(robot_mode),
|
||||||
|
Commands::Completions { shell } => handle_completions(&shell),
|
||||||
Commands::Init {
|
Commands::Init {
|
||||||
force,
|
force,
|
||||||
non_interactive,
|
non_interactive,
|
||||||
@@ -116,10 +134,12 @@ async fn main() {
|
|||||||
target_branch,
|
target_branch,
|
||||||
source_branch,
|
source_branch,
|
||||||
} => {
|
} => {
|
||||||
|
if !robot_mode {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{}",
|
"{}",
|
||||||
style("warning: 'lore list' is deprecated, use 'lore issues' or 'lore mrs'").yellow()
|
style("warning: 'lore list' is deprecated, use 'lore issues' or 'lore mrs'").yellow()
|
||||||
);
|
);
|
||||||
|
}
|
||||||
handle_list_compat(
|
handle_list_compat(
|
||||||
cli.config.as_deref(),
|
cli.config.as_deref(),
|
||||||
&entity,
|
&entity,
|
||||||
@@ -150,6 +170,7 @@ async fn main() {
|
|||||||
iid,
|
iid,
|
||||||
project,
|
project,
|
||||||
} => {
|
} => {
|
||||||
|
if !robot_mode {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{}",
|
"{}",
|
||||||
style(format!(
|
style(format!(
|
||||||
@@ -158,6 +179,7 @@ async fn main() {
|
|||||||
))
|
))
|
||||||
.yellow()
|
.yellow()
|
||||||
);
|
);
|
||||||
|
}
|
||||||
handle_show_compat(
|
handle_show_compat(
|
||||||
cli.config.as_deref(),
|
cli.config.as_deref(),
|
||||||
&entity,
|
&entity,
|
||||||
@@ -168,17 +190,21 @@ async fn main() {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
Commands::AuthTest => {
|
Commands::AuthTest => {
|
||||||
|
if !robot_mode {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{}",
|
"{}",
|
||||||
style("warning: 'lore auth-test' is deprecated, use 'lore auth'").yellow()
|
style("warning: 'lore auth-test' is deprecated, use 'lore auth'").yellow()
|
||||||
);
|
);
|
||||||
|
}
|
||||||
handle_auth_test(cli.config.as_deref(), robot_mode).await
|
handle_auth_test(cli.config.as_deref(), robot_mode).await
|
||||||
}
|
}
|
||||||
Commands::SyncStatus => {
|
Commands::SyncStatus => {
|
||||||
|
if !robot_mode {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{}",
|
"{}",
|
||||||
style("warning: 'lore sync-status' is deprecated, use 'lore status'").yellow()
|
style("warning: 'lore sync-status' is deprecated, use 'lore status'").yellow()
|
||||||
);
|
);
|
||||||
|
}
|
||||||
handle_sync_status_cmd(cli.config.as_deref(), robot_mode).await
|
handle_sync_status_cmd(cli.config.as_deref(), robot_mode).await
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -259,7 +285,10 @@ async fn handle_issues(
|
|||||||
robot_mode: bool,
|
robot_mode: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let config = Config::load(config_override)?;
|
let config = Config::load(config_override)?;
|
||||||
let order = if args.asc { "asc" } else { "desc" };
|
let asc = args.asc && !args.no_asc;
|
||||||
|
let has_due = args.has_due && !args.no_has_due;
|
||||||
|
let open = args.open && !args.no_open;
|
||||||
|
let order = if asc { "asc" } else { "desc" };
|
||||||
|
|
||||||
if let Some(iid) = args.iid {
|
if let Some(iid) = args.iid {
|
||||||
// Show mode
|
// Show mode
|
||||||
@@ -281,14 +310,14 @@ async fn handle_issues(
|
|||||||
milestone: args.milestone.as_deref(),
|
milestone: args.milestone.as_deref(),
|
||||||
since: args.since.as_deref(),
|
since: args.since.as_deref(),
|
||||||
due_before: args.due_before.as_deref(),
|
due_before: args.due_before.as_deref(),
|
||||||
has_due_date: args.has_due,
|
has_due_date: has_due,
|
||||||
sort: &args.sort,
|
sort: &args.sort,
|
||||||
order,
|
order,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = run_list_issues(&config, filters)?;
|
let result = run_list_issues(&config, filters)?;
|
||||||
|
|
||||||
if args.open {
|
if open {
|
||||||
open_issue_in_browser(&result);
|
open_issue_in_browser(&result);
|
||||||
} else if robot_mode {
|
} else if robot_mode {
|
||||||
print_list_issues_json(&result);
|
print_list_issues_json(&result);
|
||||||
@@ -306,7 +335,9 @@ async fn handle_mrs(
|
|||||||
robot_mode: bool,
|
robot_mode: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let config = Config::load(config_override)?;
|
let config = Config::load(config_override)?;
|
||||||
let order = if args.asc { "asc" } else { "desc" };
|
let asc = args.asc && !args.no_asc;
|
||||||
|
let open = args.open && !args.no_open;
|
||||||
|
let order = if asc { "asc" } else { "desc" };
|
||||||
|
|
||||||
if let Some(iid) = args.iid {
|
if let Some(iid) = args.iid {
|
||||||
// Show mode
|
// Show mode
|
||||||
@@ -337,7 +368,7 @@ async fn handle_mrs(
|
|||||||
|
|
||||||
let result = run_list_mrs(&config, filters)?;
|
let result = run_list_mrs(&config, filters)?;
|
||||||
|
|
||||||
if args.open {
|
if open {
|
||||||
open_mr_in_browser(&result);
|
open_mr_in_browser(&result);
|
||||||
} else if robot_mode {
|
} else if robot_mode {
|
||||||
print_list_mrs_json(&result);
|
print_list_mrs_json(&result);
|
||||||
@@ -353,8 +384,17 @@ async fn handle_ingest(
|
|||||||
config_override: Option<&str>,
|
config_override: Option<&str>,
|
||||||
args: IngestArgs,
|
args: IngestArgs,
|
||||||
robot_mode: bool,
|
robot_mode: bool,
|
||||||
|
quiet: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let config = Config::load(config_override)?;
|
let config = Config::load(config_override)?;
|
||||||
|
let display = if robot_mode || quiet {
|
||||||
|
IngestDisplay::silent()
|
||||||
|
} else {
|
||||||
|
IngestDisplay::interactive()
|
||||||
|
};
|
||||||
|
|
||||||
|
let force = args.force && !args.no_force;
|
||||||
|
let full = args.full && !args.no_full;
|
||||||
|
|
||||||
match args.entity.as_deref() {
|
match args.entity.as_deref() {
|
||||||
Some(resource_type) => {
|
Some(resource_type) => {
|
||||||
@@ -363,9 +403,9 @@ async fn handle_ingest(
|
|||||||
&config,
|
&config,
|
||||||
resource_type,
|
resource_type,
|
||||||
args.project.as_deref(),
|
args.project.as_deref(),
|
||||||
args.force,
|
force,
|
||||||
args.full,
|
full,
|
||||||
robot_mode,
|
display,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -377,7 +417,7 @@ async fn handle_ingest(
|
|||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
// Ingest everything: issues then MRs
|
// Ingest everything: issues then MRs
|
||||||
if !robot_mode {
|
if !robot_mode && !quiet {
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
style("Ingesting all content (issues + merge requests)...").blue()
|
style("Ingesting all content (issues + merge requests)...").blue()
|
||||||
@@ -389,9 +429,9 @@ async fn handle_ingest(
|
|||||||
&config,
|
&config,
|
||||||
"issues",
|
"issues",
|
||||||
args.project.as_deref(),
|
args.project.as_deref(),
|
||||||
args.force,
|
force,
|
||||||
args.full,
|
full,
|
||||||
robot_mode,
|
display,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -399,9 +439,9 @@ async fn handle_ingest(
|
|||||||
&config,
|
&config,
|
||||||
"mrs",
|
"mrs",
|
||||||
args.project.as_deref(),
|
args.project.as_deref(),
|
||||||
args.force,
|
force,
|
||||||
args.full,
|
full,
|
||||||
robot_mode,
|
display,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -823,65 +863,81 @@ struct VersionOutput {
|
|||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct VersionData {
|
struct VersionData {
|
||||||
version: String,
|
version: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
git_hash: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_version(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>> {
|
fn handle_version(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let version = env!("CARGO_PKG_VERSION").to_string();
|
let version = env!("CARGO_PKG_VERSION").to_string();
|
||||||
|
let git_hash = env!("GIT_HASH").to_string();
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
let output = VersionOutput {
|
let output = VersionOutput {
|
||||||
ok: true,
|
ok: true,
|
||||||
data: VersionData { version },
|
data: VersionData {
|
||||||
|
version,
|
||||||
|
git_hash: if git_hash.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(git_hash)
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
println!("{}", serde_json::to_string(&output)?);
|
println!("{}", serde_json::to_string(&output)?);
|
||||||
} else {
|
} else if git_hash.is_empty() {
|
||||||
println!("lore version {}", version);
|
println!("lore version {}", version);
|
||||||
|
} else {
|
||||||
|
println!("lore version {} ({})", version, git_hash);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// JSON output for not-implemented commands.
|
fn handle_completions(shell: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
#[derive(Serialize)]
|
use clap::CommandFactory;
|
||||||
struct NotImplementedOutput {
|
use clap_complete::{Shell, generate};
|
||||||
ok: bool,
|
|
||||||
data: NotImplementedData,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
let shell = match shell {
|
||||||
struct NotImplementedData {
|
"bash" => Shell::Bash,
|
||||||
status: String,
|
"zsh" => Shell::Zsh,
|
||||||
command: String,
|
"fish" => Shell::Fish,
|
||||||
|
"powershell" => Shell::PowerShell,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut cmd = Cli::command();
|
||||||
|
generate(shell, &mut cmd, "lore", &mut std::io::stdout());
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_backup(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>> {
|
fn handle_backup(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
let output = NotImplementedOutput {
|
let output = RobotErrorWithSuggestion {
|
||||||
ok: true,
|
error: RobotErrorSuggestionData {
|
||||||
data: NotImplementedData {
|
code: "NOT_IMPLEMENTED".to_string(),
|
||||||
status: "not_implemented".to_string(),
|
message: "The 'backup' command is not yet implemented.".to_string(),
|
||||||
command: "backup".to_string(),
|
suggestion: "Use manual database backup: cp ~/.local/share/lore/lore.db ~/.local/share/lore/lore.db.bak".to_string(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
println!("{}", serde_json::to_string(&output)?);
|
eprintln!("{}", serde_json::to_string(&output)?);
|
||||||
} else {
|
} else {
|
||||||
println!("lore backup - not yet implemented");
|
eprintln!("{} The 'backup' command is not yet implemented.", style("Error:").red());
|
||||||
}
|
}
|
||||||
Ok(())
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_reset(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>> {
|
fn handle_reset(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
let output = NotImplementedOutput {
|
let output = RobotErrorWithSuggestion {
|
||||||
ok: true,
|
error: RobotErrorSuggestionData {
|
||||||
data: NotImplementedData {
|
code: "NOT_IMPLEMENTED".to_string(),
|
||||||
status: "not_implemented".to_string(),
|
message: "The 'reset' command is not yet implemented.".to_string(),
|
||||||
command: "reset".to_string(),
|
suggestion: "Manually delete the database: rm ~/.local/share/lore/lore.db".to_string(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
println!("{}", serde_json::to_string(&output)?);
|
eprintln!("{}", serde_json::to_string(&output)?);
|
||||||
} else {
|
} else {
|
||||||
println!("lore reset - not yet implemented");
|
eprintln!("{} The 'reset' command is not yet implemented.", style("Error:").red());
|
||||||
}
|
}
|
||||||
Ok(())
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// JSON output for migrate command.
|
/// JSON output for migrate command.
|
||||||
@@ -987,7 +1043,9 @@ async fn handle_stats(
|
|||||||
robot_mode: bool,
|
robot_mode: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let config = Config::load(config_override)?;
|
let config = Config::load(config_override)?;
|
||||||
let result = run_stats(&config, args.check, args.repair)?;
|
// Auto-enable --check when --repair is used
|
||||||
|
let check = (args.check && !args.no_check) || args.repair;
|
||||||
|
let result = run_stats(&config, check, args.repair)?;
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
print_stats_json(&result);
|
print_stats_json(&result);
|
||||||
} else {
|
} else {
|
||||||
@@ -1002,6 +1060,7 @@ async fn handle_search(
|
|||||||
robot_mode: bool,
|
robot_mode: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let config = Config::load(config_override)?;
|
let config = Config::load(config_override)?;
|
||||||
|
let explain = args.explain && !args.no_explain;
|
||||||
|
|
||||||
let fts_mode = match args.fts_mode.as_str() {
|
let fts_mode = match args.fts_mode.as_str() {
|
||||||
"raw" => lore::search::FtsQueryMode::Raw,
|
"raw" => lore::search::FtsQueryMode::Raw,
|
||||||
@@ -1020,7 +1079,7 @@ async fn handle_search(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
let response = run_search(&config, &args.query, cli_filters, fts_mode, args.explain)?;
|
let response = run_search(&config, &args.query, cli_filters, fts_mode, explain)?;
|
||||||
let elapsed_ms = start.elapsed().as_millis() as u64;
|
let elapsed_ms = start.elapsed().as_millis() as u64;
|
||||||
|
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
@@ -1053,7 +1112,8 @@ async fn handle_embed(
|
|||||||
robot_mode: bool,
|
robot_mode: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let config = Config::load(config_override)?;
|
let config = Config::load(config_override)?;
|
||||||
let result = run_embed(&config, args.retry_failed).await?;
|
let retry_failed = args.retry_failed && !args.no_retry_failed;
|
||||||
|
let result = run_embed(&config, retry_failed).await?;
|
||||||
if robot_mode {
|
if robot_mode {
|
||||||
print_embed_json(&result);
|
print_embed_json(&result);
|
||||||
} else {
|
} else {
|
||||||
@@ -1069,10 +1129,11 @@ async fn handle_sync_cmd(
|
|||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let config = Config::load(config_override)?;
|
let config = Config::load(config_override)?;
|
||||||
let options = SyncOptions {
|
let options = SyncOptions {
|
||||||
full: args.full,
|
full: args.full && !args.no_full,
|
||||||
force: args.force,
|
force: args.force && !args.no_force,
|
||||||
no_embed: args.no_embed,
|
no_embed: args.no_embed,
|
||||||
no_docs: args.no_docs,
|
no_docs: args.no_docs,
|
||||||
|
robot_mode,
|
||||||
};
|
};
|
||||||
|
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
@@ -1301,8 +1362,8 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>>
|
|||||||
|
|
||||||
let exit_codes = serde_json::json!({
|
let exit_codes = serde_json::json!({
|
||||||
"0": "Success",
|
"0": "Success",
|
||||||
"1": "Internal error / health check failed",
|
"1": "Internal error / health check failed / not implemented",
|
||||||
"2": "Config not found / missing flags",
|
"2": "Usage error (invalid flags or arguments)",
|
||||||
"3": "Config invalid",
|
"3": "Config invalid",
|
||||||
"4": "Token not set",
|
"4": "Token not set",
|
||||||
"5": "GitLab auth failed",
|
"5": "GitLab auth failed",
|
||||||
@@ -1313,7 +1374,13 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>>
|
|||||||
"10": "Database error",
|
"10": "Database error",
|
||||||
"11": "Migration failed",
|
"11": "Migration failed",
|
||||||
"12": "I/O error",
|
"12": "I/O error",
|
||||||
"13": "Transform error"
|
"13": "Transform error",
|
||||||
|
"14": "Ollama unavailable",
|
||||||
|
"15": "Ollama model not found",
|
||||||
|
"16": "Embedding failed",
|
||||||
|
"17": "Not found",
|
||||||
|
"18": "Ambiguous match",
|
||||||
|
"20": "Config not found"
|
||||||
});
|
});
|
||||||
|
|
||||||
let workflows = serde_json::json!({
|
let workflows = serde_json::json!({
|
||||||
|
|||||||
Reference in New Issue
Block a user