Files
gitlore/src/main.rs
teernisse e8d6c5b15f feat(runtime): replace tokio+reqwest with asupersync async runtime
- Add HTTP adapter layer (src/http.rs) wrapping asupersync h1 client
- Migrate gitlab client, graphql, and ollama to HTTP adapter
- Swap entrypoint from #[tokio::main] to RuntimeBuilder::new().block_on()
- Rewrite signal handler for asupersync (RuntimeHandle::spawn + ctrl_c())
- Migrate rate limiter sleeps to asupersync::time::sleep(wall_now(), d)
- Add asupersync-native HTTP integration tests
- Convert timeline_seed_tests to RuntimeBuilder pattern

Phases 1-3 of asupersync migration (atomic: code won't compile without all pieces).
2026-03-06 15:57:20 -05:00

432 lines
16 KiB
Rust

#![recursion_limit = "256"]
use clap::Parser;
use dialoguer::{Confirm, Input};
use serde::Serialize;
use strsim::jaro_winkler;
use tracing_subscriber::Layer;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use lore::Config;
use lore::cli::autocorrect::{self, CorrectionResult};
use lore::cli::commands::{
IngestDisplay, InitInputs, InitOptions, InitResult, ListFilters, MrListFilters,
NoteListFilters, RefreshOptions, RefreshResult, SearchCliFilters, SyncOptions, TimelineParams,
delete_orphan_projects, open_issue_in_browser, open_mr_in_browser, parse_trace_path,
print_count, print_count_json, print_cron_install, print_cron_install_json, print_cron_status,
print_cron_status_json, print_cron_uninstall, print_cron_uninstall_json, print_doctor_results,
print_drift_human, print_drift_json, print_dry_run_preview, print_dry_run_preview_json,
print_embed, print_embed_json, print_event_count, print_event_count_json, print_file_history,
print_file_history_json, print_generate_docs, print_generate_docs_json, print_ingest_summary,
print_ingest_summary_json, print_list_issues, print_list_issues_json, print_list_mrs,
print_list_mrs_json, print_list_notes, print_list_notes_json, print_related_human,
print_related_json, print_search_results, print_search_results_json, print_show_issue,
print_show_issue_json, print_show_mr, print_show_mr_json, print_stats, print_stats_json,
print_sync, print_sync_json, print_sync_status, print_sync_status_json, print_timeline,
print_timeline_json_with_meta, print_trace, print_trace_json, print_who_human, print_who_json,
query_notes, run_auth_test, run_count, run_count_events, run_cron_install, run_cron_status,
run_cron_uninstall, run_doctor, run_drift, run_embed, run_file_history, run_generate_docs,
run_ingest, run_ingest_dry_run, run_init, run_init_refresh, run_list_issues, run_list_mrs,
run_me, run_related, run_search, run_show_issue, run_show_mr, run_stats, run_sync,
run_sync_status, run_timeline, run_token_set, run_token_show, run_who,
};
use lore::cli::render::{ColorMode, GlyphMode, Icons, LoreRenderer, Theme};
use lore::cli::robot::{RobotMeta, strip_schemas};
use lore::cli::{
Cli, Commands, CountArgs, CronAction, CronArgs, EmbedArgs, FileHistoryArgs, GenerateDocsArgs,
IngestArgs, IssuesArgs, MeArgs, MrsArgs, NotesArgs, SearchArgs, StatsArgs, SyncArgs,
TimelineArgs, TokenAction, TokenArgs, TraceArgs, WhoArgs,
};
use lore::core::db::{
LATEST_SCHEMA_VERSION, create_connection, get_schema_version, run_migrations,
};
use lore::core::error::{LoreError, RobotErrorOutput};
use lore::core::logging;
use lore::core::metrics::MetricsLayer;
use lore::core::path_resolver::{build_path_query, normalize_repo_path};
use lore::core::paths::{get_config_path, get_db_path, get_log_dir};
use lore::core::project::resolve_project;
use lore::core::shutdown::{ShutdownSignal, install_ctrl_c_handler};
use lore::core::trace::run_trace;
use lore::ingestion::storage::queue::release_all_locked_jobs;
use lore::ingestion::storage::sync_run::SyncRunRecorder;
fn main() {
let rt = match asupersync::runtime::RuntimeBuilder::new().build() {
Ok(rt) => rt,
Err(e) => {
eprintln!("Failed to build async runtime: {e}");
std::process::exit(1);
}
};
let rt_handle = rt.handle();
rt.block_on(async {
#[cfg(unix)]
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
// Phase 1: Early robot mode detection for structured clap errors
let robot_mode_early = Cli::detect_robot_mode_from_env();
// Phase 1.5: Pre-clap arg correction for agent typo tolerance
let raw_args: Vec<String> = std::env::args().collect();
let correction_result = autocorrect::correct_args(raw_args, robot_mode_early);
// Emit correction warnings to stderr (before clap parsing, so they appear
// even if clap still fails on something else)
if !correction_result.corrections.is_empty() {
emit_correction_warnings(&correction_result, robot_mode_early);
}
let cli = match Cli::try_parse_from(&correction_result.args) {
Ok(cli) => cli,
Err(e) => {
handle_clap_error(e, robot_mode_early, &correction_result);
}
};
let robot_mode = cli.is_robot_mode();
let logging_config = lore::Config::load(cli.config.as_deref())
.map(|c| c.logging)
.unwrap_or_default();
let log_dir = get_log_dir(logging_config.log_dir.as_deref());
if logging_config.file_logging && logging_config.retention_days > 0 {
logging::cleanup_old_logs(&log_dir, logging_config.retention_days);
}
let stderr_filter = logging::build_stderr_filter(cli.verbose, cli.quiet);
let metrics_layer = MetricsLayer::new();
let registry = tracing_subscriber::registry();
let _file_guard: Option<tracing_appender::non_blocking::WorkerGuard>;
if cli.log_format == "json" {
let stderr_layer = tracing_subscriber::fmt::layer()
.json()
.with_writer(lore::cli::progress::SuspendingWriter)
.with_filter(stderr_filter);
if logging_config.file_logging {
let file_filter = logging::build_file_filter();
std::fs::create_dir_all(&log_dir).ok();
let file_appender = tracing_appender::rolling::daily(&log_dir, "lore");
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
_file_guard = Some(guard);
let file_layer = tracing_subscriber::fmt::layer()
.json()
.with_writer(non_blocking)
.with_filter(file_filter);
registry
.with(stderr_layer)
.with(file_layer)
.with(metrics_layer.clone())
.init();
} else {
_file_guard = None;
registry
.with(stderr_layer)
.with(metrics_layer.clone())
.init();
}
} else {
let stderr_layer = tracing_subscriber::fmt::layer()
.event_format(logging::CompactHumanFormat)
.with_writer(lore::cli::progress::SuspendingWriter)
.with_filter(stderr_filter);
if logging_config.file_logging {
let file_filter = logging::build_file_filter();
std::fs::create_dir_all(&log_dir).ok();
let file_appender = tracing_appender::rolling::daily(&log_dir, "lore");
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
_file_guard = Some(guard);
let file_layer = tracing_subscriber::fmt::layer()
.json()
.with_writer(non_blocking)
.with_filter(file_filter);
registry
.with(stderr_layer)
.with(file_layer)
.with(metrics_layer.clone())
.init();
} else {
_file_guard = None;
registry
.with(stderr_layer)
.with(metrics_layer.clone())
.init();
}
}
// Icon mode is independent of color flags; robot mode still defaults to ASCII.
let glyphs = GlyphMode::detect(cli.icons.as_deref(), robot_mode);
if std::env::var("NO_COLOR").is_ok_and(|v| !v.is_empty()) {
LoreRenderer::init(ColorMode::Never, glyphs);
console::set_colors_enabled(false);
} else {
match cli.color.as_str() {
"never" => {
LoreRenderer::init(ColorMode::Never, glyphs);
console::set_colors_enabled(false);
}
"always" => {
LoreRenderer::init(ColorMode::Always, glyphs);
console::set_colors_enabled(true);
}
"auto" => {
LoreRenderer::init(ColorMode::Auto, glyphs);
}
other => {
LoreRenderer::init(ColorMode::Auto, glyphs);
eprintln!("Warning: unknown color mode '{}', using auto", other);
}
}
}
let quiet = cli.quiet;
let result = match cli.command {
// Phase 2: Handle no-args case - in robot mode, output robot-docs; otherwise show help
None => {
if robot_mode {
handle_robot_docs(robot_mode, false)
} else {
use clap::CommandFactory;
let mut cmd = Cli::command();
cmd.print_help().ok();
println!();
Ok(())
}
}
Some(Commands::Issues(args)) => handle_issues(cli.config.as_deref(), args, robot_mode),
Some(Commands::Mrs(args)) => handle_mrs(cli.config.as_deref(), args, robot_mode),
Some(Commands::Notes(args)) => handle_notes(cli.config.as_deref(), args, robot_mode),
Some(Commands::Search(args)) => {
handle_search(cli.config.as_deref(), args, robot_mode).await
}
Some(Commands::Timeline(args)) => {
handle_timeline(cli.config.as_deref(), args, robot_mode).await
}
Some(Commands::Who(args)) => handle_who(cli.config.as_deref(), args, robot_mode),
Some(Commands::Me(args)) => handle_me(cli.config.as_deref(), args, robot_mode),
Some(Commands::FileHistory(args)) => {
handle_file_history(cli.config.as_deref(), args, robot_mode)
}
Some(Commands::Trace(args)) => handle_trace(cli.config.as_deref(), args, robot_mode),
Some(Commands::Cron(args)) => handle_cron(cli.config.as_deref(), args, robot_mode),
Some(Commands::Token(args)) => handle_token(cli.config.as_deref(), args, robot_mode).await,
Some(Commands::Drift {
entity_type,
iid,
threshold,
project,
}) => {
handle_drift(
cli.config.as_deref(),
&entity_type,
iid,
threshold,
project.as_deref(),
robot_mode,
)
.await
}
Some(Commands::Related {
query_or_type,
iid,
limit,
project,
}) => {
handle_related(
cli.config.as_deref(),
&query_or_type,
iid,
limit,
project.as_deref(),
robot_mode,
)
.await
}
Some(Commands::Stats(args)) => handle_stats(cli.config.as_deref(), args, robot_mode).await,
Some(Commands::Embed(args)) => handle_embed(cli.config.as_deref(), args, robot_mode, &rt_handle).await,
Some(Commands::Sync(args)) => {
handle_sync_cmd(cli.config.as_deref(), args, robot_mode, &metrics_layer, &rt_handle).await
}
Some(Commands::Ingest(args)) => {
handle_ingest(
cli.config.as_deref(),
args,
robot_mode,
quiet,
&metrics_layer,
&rt_handle,
)
.await
}
Some(Commands::Count(args)) => handle_count(cli.config.as_deref(), args, robot_mode).await,
Some(Commands::Status) => handle_sync_status_cmd(cli.config.as_deref(), robot_mode).await,
Some(Commands::Auth) => handle_auth_test(cli.config.as_deref(), robot_mode).await,
Some(Commands::Doctor) => handle_doctor(cli.config.as_deref(), robot_mode).await,
Some(Commands::Version) => handle_version(robot_mode),
Some(Commands::Completions { shell }) => handle_completions(&shell),
Some(Commands::Init {
refresh,
force,
non_interactive,
gitlab_url,
token_env_var,
projects,
default_project,
}) => {
handle_init(
cli.config.as_deref(),
refresh,
force,
non_interactive,
robot_mode,
gitlab_url,
token_env_var,
projects,
default_project,
)
.await
}
Some(Commands::GenerateDocs(args)) => {
handle_generate_docs(cli.config.as_deref(), args, robot_mode).await
}
Some(Commands::Backup) => handle_backup(robot_mode),
Some(Commands::Reset { yes: _ }) => handle_reset(robot_mode),
Some(Commands::Migrate) => handle_migrate(cli.config.as_deref(), robot_mode).await,
Some(Commands::Health) => handle_health(cli.config.as_deref(), robot_mode).await,
Some(Commands::RobotDocs { brief }) => handle_robot_docs(robot_mode, brief),
Some(Commands::List {
entity,
limit,
project,
state,
author,
assignee,
label,
milestone,
since,
due_before,
has_due_date,
sort,
order,
open,
draft,
no_draft,
reviewer,
target_branch,
source_branch,
}) => {
if robot_mode {
eprintln!(
r#"{{"warning":{{"type":"DEPRECATED","message":"'lore list' is deprecated, use 'lore issues' or 'lore mrs'","successor":"issues / mrs"}}}}"#
);
} else {
eprintln!(
"{}",
Theme::warning().render(
"warning: 'lore list' is deprecated, use 'lore issues' or 'lore mrs'"
)
);
}
handle_list_compat(
cli.config.as_deref(),
&entity,
limit,
project.as_deref(),
state.as_deref(),
author.as_deref(),
assignee.as_deref(),
label.as_deref(),
milestone.as_deref(),
since.as_deref(),
due_before.as_deref(),
has_due_date,
&sort,
&order,
open,
robot_mode,
draft,
no_draft,
reviewer.as_deref(),
target_branch.as_deref(),
source_branch.as_deref(),
)
.await
}
Some(Commands::Show {
entity,
iid,
project,
}) => {
if robot_mode {
eprintln!(
r#"{{"warning":{{"type":"DEPRECATED","message":"'lore show' is deprecated, use 'lore {entity}s {iid}'","successor":"{entity}s"}}}}"#
);
} else {
eprintln!(
"{}",
Theme::warning().render(&format!(
"warning: 'lore show' is deprecated, use 'lore {}s {}'",
entity, iid
))
);
}
handle_show_compat(
cli.config.as_deref(),
&entity,
iid,
project.as_deref(),
robot_mode,
)
.await
}
Some(Commands::AuthTest) => {
if robot_mode {
eprintln!(
r#"{{"warning":{{"type":"DEPRECATED","message":"'lore auth-test' is deprecated, use 'lore auth'","successor":"auth"}}}}"#
);
} else {
eprintln!(
"{}",
Theme::warning()
.render("warning: 'lore auth-test' is deprecated, use 'lore auth'")
);
}
handle_auth_test(cli.config.as_deref(), robot_mode).await
}
Some(Commands::SyncStatus) => {
if robot_mode {
eprintln!(
r#"{{"warning":{{"type":"DEPRECATED","message":"'lore sync-status' is deprecated, use 'lore status'","successor":"status"}}}}"#
);
} else {
eprintln!(
"{}",
Theme::warning()
.render("warning: 'lore sync-status' is deprecated, use 'lore status'")
);
}
handle_sync_status_cmd(cli.config.as_deref(), robot_mode).await
}
};
if let Err(e) = result {
handle_error(e, robot_mode);
}
}); // rt.block_on
}
include!("app/dispatch.rs");