Files
gitlore/src/cli/commands/init.rs
Taylor Eernisse daf5a73019 feat(cli): Add search, stats, embed, sync, health, and robot-docs commands
Extends the CLI with six new commands that complete the search pipeline:

- lore search <QUERY>: Hybrid search with mode selection (lexical,
  hybrid, semantic), rich filtering (--type, --author, --project,
  --label, --path, --after, --updated-after), result limits, and
  optional explain mode showing RRF score breakdowns. Safe FTS mode
  sanitizes user input; raw mode passes through for power users.

- lore stats: Document and index statistics with optional --check
  for integrity verification and --repair to fix inconsistencies
  (orphaned documents, missing FTS entries, stale dirty queue items).

- lore embed: Generate vector embeddings via Ollama. Supports
  --retry-failed to re-attempt previously failed embeddings.

- lore generate-docs: Drain the dirty queue to regenerate documents.
  --full seeds all entities for complete rebuild. --project scopes
  to a single project.

- lore sync: Full pipeline orchestration (ingest issues + MRs,
  generate-docs, embed) with --no-embed and --no-docs flags for
  partial runs. Reports per-stage results and total elapsed time.

- lore health: Quick pre-flight check (config exists, DB exists,
  schema current). Returns exit code 1 if unhealthy. Designed for
  agent pre-flight scripts.

- lore robot-docs: Machine-readable command manifest for agent
  self-discovery. Returns all commands, flags, examples, exit codes,
  and recommended workflows as structured JSON.

Also enhances lore init with --gitlab-url, --token-env-var, and
--projects flags for fully non-interactive robot-mode initialization.
Fixes init's force/non-interactive precedence logic and adds JSON
output for robot mode.

Updates all command files for the GiError -> LoreError rename.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 15:47:10 -05:00

171 lines
5.0 KiB
Rust

//! Init command - initialize configuration and database.
use std::fs;
use crate::core::config::{MinimalConfig, MinimalGitLabConfig, ProjectConfig};
use crate::core::db::{create_connection, run_migrations};
use crate::core::error::{LoreError, Result};
use crate::core::paths::{get_config_path, get_data_dir};
use crate::gitlab::{GitLabClient, GitLabProject};
/// Input data for init command.
pub struct InitInputs {
pub gitlab_url: String,
pub token_env_var: String,
pub project_paths: Vec<String>,
}
/// Options for init command.
pub struct InitOptions {
pub config_path: Option<String>,
pub force: bool,
pub non_interactive: bool,
}
/// Result of successful init.
pub struct InitResult {
pub config_path: String,
pub data_dir: String,
pub user: UserInfo,
pub projects: Vec<ProjectInfo>,
}
pub struct UserInfo {
pub username: String,
pub name: String,
}
pub struct ProjectInfo {
pub path: String,
pub name: String,
}
/// Run the init command programmatically.
pub async fn run_init(inputs: InitInputs, options: InitOptions) -> Result<InitResult> {
let config_path = get_config_path(options.config_path.as_deref());
let data_dir = get_data_dir();
// 1. Check if config exists (force takes precedence over non_interactive)
if config_path.exists() && !options.force {
if options.non_interactive {
return Err(LoreError::Other(format!(
"Config file exists at {}. Use --force to overwrite.",
config_path.display()
)));
}
return Err(LoreError::Other(
"User cancelled config overwrite.".to_string(),
));
}
// 2. Validate GitLab URL format
if url::Url::parse(&inputs.gitlab_url).is_err() {
return Err(LoreError::Other(format!(
"Invalid GitLab URL: {}",
inputs.gitlab_url
)));
}
// 3. Check token is set in environment
let token = std::env::var(&inputs.token_env_var).map_err(|_| LoreError::TokenNotSet {
env_var: inputs.token_env_var.clone(),
})?;
// 4. Create GitLab client and test authentication
let client = GitLabClient::new(&inputs.gitlab_url, &token, None);
let gitlab_user = client.get_current_user().await.map_err(|e| {
if matches!(e, LoreError::GitLabAuthFailed) {
LoreError::Other(format!("Authentication failed for {}", inputs.gitlab_url))
} else {
e
}
})?;
let user = UserInfo {
username: gitlab_user.username,
name: gitlab_user.name,
};
// 5. Validate each project path
let mut validated_projects: Vec<(ProjectInfo, GitLabProject)> = Vec::new();
for project_path in &inputs.project_paths {
let project = client.get_project(project_path).await.map_err(|e| {
if matches!(e, LoreError::GitLabNotFound { .. }) {
LoreError::Other(format!("Project not found: {project_path}"))
} else {
e
}
})?;
validated_projects.push((
ProjectInfo {
path: project_path.clone(),
name: project.name.clone().unwrap_or_else(|| {
project_path
.split('/')
.next_back()
.unwrap_or(project_path)
.to_string()
}),
},
project,
));
}
// 6. All validations passed - now write config and setup DB
// Create config directory if needed
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent)?;
}
// Write minimal config (rely on serde defaults)
let config = MinimalConfig {
gitlab: MinimalGitLabConfig {
base_url: inputs.gitlab_url,
token_env_var: inputs.token_env_var,
},
projects: inputs
.project_paths
.iter()
.map(|p| ProjectConfig { path: p.clone() })
.collect(),
};
let config_json = serde_json::to_string_pretty(&config)?;
fs::write(&config_path, format!("{config_json}\n"))?;
// 7. Create data directory and initialize database
fs::create_dir_all(&data_dir)?;
let db_path = data_dir.join("lore.db");
let conn = create_connection(&db_path)?;
// Run embedded migrations
run_migrations(&conn)?;
// 8. Insert validated projects
for (_, gitlab_project) in &validated_projects {
conn.execute(
"INSERT INTO projects (gitlab_project_id, path_with_namespace, default_branch, web_url)
VALUES (?, ?, ?, ?)",
(
gitlab_project.id,
&gitlab_project.path_with_namespace,
&gitlab_project.default_branch,
&gitlab_project.web_url,
),
)?;
}
Ok(InitResult {
config_path: config_path.display().to_string(),
data_dir: data_dir.display().to_string(),
user,
projects: validated_projects.into_iter().map(|(p, _)| p).collect(),
})
}