feat(cli): Implement complete command-line interface
Provides a user-friendly CLI for all GitLab Inbox operations. src/cli/mod.rs - Clap command definitions: - Global --config flag for alternate config path - Subcommands: init, auth-test, doctor, version, backup, reset, migrate, sync-status, ingest, list, count, show - Ingest supports --type (issues/merge_requests), --project filter, --force lock override, --full resync - List supports rich filtering: --state, --author, --assignee, --label, --milestone, --since, --due-before, --has-due-date - List supports --sort (updated/created/iid), --order (asc/desc) - List supports --open to launch browser, --json for scripting src/cli/commands/ - Command implementations: init.rs: Interactive configuration wizard - Prompts for GitLab URL, token env var, projects to track - Creates config file and initializes database - Supports --force overwrite and --non-interactive mode auth_test.rs: Verify GitLab authentication - Calls /api/v4/user to validate token - Displays username and GitLab instance URL doctor.rs: Environment health check - Validates config file exists and parses correctly - Checks database connectivity and migration state - Verifies GitLab authentication - Reports token environment variable status - Supports --json output for CI integration ingest.rs: Data synchronization from GitLab - Acquires sync lock with stale detection - Shows progress bars for issues and discussions - Reports sync statistics on completion - Supports --full flag to reset cursors and refetch all data list.rs: Query local database - Formatted table output with comfy-table - Filters build dynamic SQL with parameterized queries - Username filters normalize @ prefix automatically - --open flag uses 'open' crate for cross-platform browser launch - --json outputs array of issue objects show.rs: Detailed entity view - Displays issue metadata in structured format - Shows full description with markdown - Lists labels, assignees, milestone - Shows discussion threads with notes count.rs: Entity statistics - Counts issues, discussions, or notes - Supports --type filter for discussions/notes sync_status.rs: Display sync watermarks - Shows last sync time per project - Displays cursor positions for debugging src/main.rs - Application entry point: - Initializes tracing subscriber with env-filter - Parses CLI arguments via clap - Dispatches to appropriate command handler - Consistent error formatting for all failure modes src/lib.rs - Library entry point: - Exports cli, core, gitlab, ingestion modules - Re-exports Config, GiError, Result for convenience Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
172
src/cli/commands/init.rs
Normal file
172
src/cli/commands/init.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
//! 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::{GiError, 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
|
||||
if config_path.exists() {
|
||||
if options.non_interactive {
|
||||
return Err(GiError::Other(format!(
|
||||
"Config file exists at {}. Cannot proceed in non-interactive mode.",
|
||||
config_path.display()
|
||||
)));
|
||||
}
|
||||
|
||||
if !options.force {
|
||||
return Err(GiError::Other(
|
||||
"User cancelled config overwrite.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Validate GitLab URL format
|
||||
if url::Url::parse(&inputs.gitlab_url).is_err() {
|
||||
return Err(GiError::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(|_| GiError::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, GiError::GitLabAuthFailed) {
|
||||
GiError::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, GiError::GitLabNotFound { .. }) {
|
||||
GiError::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("gi.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(),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user