- 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).
432 lines
16 KiB
Rust
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");
|