diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ee893e..91a4a52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: - name: Restore and save Nix store uses: nix-community/cache-nix-action@v7 with: - primary-key: nix-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('flake.nix', 'flake.lock', 'Cargo.lock', 'Cargo.toml') }}-${{ hashFiles('src/**', 'tests/**', 'scripts/**', 'install.sh', 'README.md', 'LICENSE') }} + primary-key: nix-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('flake.nix', 'flake.lock', 'Cargo.lock', 'Cargo.toml') }}-${{ hashFiles('crates/**', 'scripts/**', 'install.sh', 'README.md', 'LICENSE') }} restore-prefixes-first-match: | nix-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('flake.nix', 'flake.lock', 'Cargo.lock', 'Cargo.toml') }}- nix-v2-${{ runner.os }}-${{ runner.arch }}- diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 887cfcb..f18dc43 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,6 +13,7 @@ on: paths: - ".github/workflows/release.yml" - "Cargo.toml" + - "crates/*/Cargo.toml" - "Cargo.lock" - "package.json" - "scripts/install.js" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dfb9713..f1c407f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,9 +3,9 @@ repos: hooks: # Golden files pin exact CLI output, including trailing spaces. - id: trailing-whitespace - exclude: ^tests/golden/.*\.golden$ + exclude: ^crates/idx-cli/tests/golden/.*\.golden$ - id: end-of-file-fixer - exclude: ^tests/golden/.*\.golden$ + exclude: ^crates/idx-cli/tests/golden/.*\.golden$ - id: check-merge-conflict - repo: local diff --git a/AGENTS.md b/AGENTS.md index 70b7779..122a90a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,16 +5,19 @@ ## Navigation ```text -src/ -├── main.rs, config.rs, cache.rs, error.rs, curl_impersonate.rs -├── cli/ clap commands: stocks, ownership, config, cache -├── api/ providers: msn/ (default), yahoo/ (history/OHLCV) -├── analysis/ technical, fundamental, signals -├── ownership/ xlsx + pdf parsers, archive (ZIP/TXT), remote discovery, -│ snapshot sync, db (SQLite/FTS5), entities, search, graph -└── output/ table.rs, json.rs -tests/ cli.rs (behavior), golden.rs + golden/ (output parity), fixtures/ -scripts/ smoke, installer, and ownership snapshot publisher +crates/idx-core/ library: all data access and analysis +├── src/api/ providers: msn/ (default), yahoo/ (history/OHLCV) +├── src/analysis/ technical, fundamental, signals +├── src/ownership/ xlsx + pdf parsers, archive (ZIP/TXT), remote discovery, +│ snapshot sync, db (SQLite/FTS5), entities, search, graph +├── src/{cache,config,error,curl_impersonate}.rs, lib.rs (runtime) +└── tests/fixtures/ shared fixtures (also read by the mock provider) +crates/idx-cli/ binary `idx` +├── src/cli/ clap commands: stocks, ownership, config, cache +├── src/config.rs IdxConfig: output format and color resolution +├── src/output/ table.rs, json.rs, style.rs +└── tests/ cli.rs (behavior), golden.rs + golden/ (output parity) +scripts/ smoke, installer, and ownership snapshot publisher ``` Docs: diff --git a/Cargo.lock b/Cargo.lock index d9b9acf..f8f727a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -718,10 +718,26 @@ dependencies = [ "clap", "clap_complete", "comfy-table", - "directories", - "fastrand", + "idx-core", "owo-colors", "predicates", + "rusqlite", + "serde", + "serde_json", + "sha2", + "toml", + "ureq", + "zip", +] + +[[package]] +name = "idx-core" +version = "0.2.4" +dependencies = [ + "chrono", + "clap", + "directories", + "fastrand", "quick-xml", "rusqlite", "serde", diff --git a/Cargo.toml b/Cargo.toml index 0cac163..68c9bd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,50 +1,23 @@ -[package] -name = "idx-cli" +[workspace] +resolver = "3" +members = ["crates/idx-core", "crates/idx-cli"] + +[workspace.package] version = "0.2.4" edition = "2024" rust-version = "1.85" -description = "CLI tool for Indonesian stock market (IDX) analysis" license = "MIT" repository = "https://github.com/0xrsydn/idx-cli" homepage = "https://github.com/0xrsydn/idx-cli" -keywords = ["idx", "stocks", "indonesia", "cli", "finance"] -categories = ["command-line-utilities", "finance"] -include = [ - "/Cargo.toml", - "/Cargo.lock", - "/LICENSE", - "/README.md", - "/src/**", - "/tests/cli.rs", - "/tests/fixtures/**", -] -[[bin]] -name = "idx" -path = "src/main.rs" - -[features] -default = ["ownership"] -ownership = ["dep:rusqlite", "dep:quick-xml", "dep:sha2", "dep:zip"] - -[dependencies] +[workspace.dependencies] +idx-core = { path = "crates/idx-core", version = "=0.2.4" } clap = { version = "4", features = ["derive"] } -clap_complete = "4" -ureq = { version = "3", features = ["json", "cookies"] } serde = { version = "1", features = ["derive"] } serde_json = { version = "1", features = ["raw_value"] } -comfy-table = "7" -owo-colors = "4" toml = "0.8" -directories = "5" chrono = { version = "0.4", features = ["serde"] } -thiserror = "2" -fastrand = "2" -rusqlite = { version = "0.32", features = ["bundled-full"], optional = true } -quick-xml = { version = "0.37", optional = true } -sha2 = { version = "0.10", optional = true } -zip = { version = "2", default-features = false, features = ["deflate"], optional = true } - -[dev-dependencies] -assert_cmd = "2" -predicates = "3" +ureq = { version = "3", features = ["json", "cookies"] } +rusqlite = { version = "0.32", features = ["bundled-full"] } +sha2 = "0.10" +zip = { version = "2", default-features = false, features = ["deflate"] } diff --git a/crates/idx-cli/Cargo.toml b/crates/idx-cli/Cargo.toml new file mode 100644 index 0000000..53fa329 --- /dev/null +++ b/crates/idx-cli/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "idx-cli" +description = "CLI tool for Indonesian stock market (IDX) analysis" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +readme = "../../README.md" +keywords = ["idx", "stocks", "indonesia", "cli", "finance"] +categories = ["command-line-utilities", "finance"] +include = ["/Cargo.toml", "/LICENSE", "/src/**"] + +[[bin]] +name = "idx" +path = "src/main.rs" + +[features] +default = ["ownership"] +# rusqlite/sha2/ureq back ownership import code that phase 3 moves into idx-core. +ownership = ["idx-core/ownership", "dep:rusqlite", "dep:sha2", "dep:ureq"] + +[dependencies] +idx-core = { workspace = true, features = ["clap"] } +clap.workspace = true +clap_complete = "4" +serde.workspace = true +serde_json.workspace = true +comfy-table = "7" +owo-colors = "4" +toml.workspace = true +chrono.workspace = true +rusqlite = { workspace = true, optional = true } +sha2 = { workspace = true, optional = true } +ureq = { workspace = true, optional = true } + +[dev-dependencies] +assert_cmd = "2" +predicates = "3" +rusqlite.workspace = true +sha2.workspace = true +zip.workspace = true diff --git a/crates/idx-cli/LICENSE b/crates/idx-cli/LICENSE new file mode 120000 index 0000000..30cff74 --- /dev/null +++ b/crates/idx-cli/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/src/cli/cache.rs b/crates/idx-cli/src/cli/cache.rs similarity index 96% rename from src/cli/cache.rs rename to crates/idx-cli/src/cli/cache.rs index 8b190a7..f542d10 100644 --- a/src/cli/cache.rs +++ b/crates/idx-cli/src/cli/cache.rs @@ -1,9 +1,9 @@ use clap::{Args, Subcommand}; -use crate::cache::Cache; -use crate::error::IdxError; use crate::output::{OutputFormat, json}; -use crate::runtime; +use idx_core::cache::Cache; +use idx_core::error::IdxError; +use idx_core::runtime; #[derive(Debug, Args)] #[command(about = "Manage local cache")] diff --git a/src/cli/config.rs b/crates/idx-cli/src/cli/config.rs similarity index 97% rename from src/cli/config.rs rename to crates/idx-cli/src/cli/config.rs index 17f7cc7..cd77da2 100644 --- a/src/cli/config.rs +++ b/crates/idx-cli/src/cli/config.rs @@ -1,7 +1,7 @@ use clap::{Args, Subcommand}; use crate::config::{config_path, ensure_default_config, get_config_value, set_config_value}; -use crate::error::IdxError; +use idx_core::error::IdxError; #[derive(Debug, Args)] #[command(about = "Manage configuration")] diff --git a/src/cli/mod.rs b/crates/idx-cli/src/cli/mod.rs similarity index 100% rename from src/cli/mod.rs rename to crates/idx-cli/src/cli/mod.rs diff --git a/src/cli/ownership.rs b/crates/idx-cli/src/cli/ownership.rs similarity index 97% rename from src/cli/ownership.rs rename to crates/idx-cli/src/cli/ownership.rs index eeadaea..f5d33b5 100644 --- a/src/cli/ownership.rs +++ b/crates/idx-cli/src/cli/ownership.rs @@ -10,13 +10,13 @@ use serde::Serialize; use sha2::{Digest, Sha256}; use crate::config::IdxConfig; -use crate::error::IdxError; use crate::output::json; use crate::output::table::format_idr; -use crate::ownership::types::{ +use idx_core::error::IdxError; +use idx_core::ownership::types::{ ChangeType, FlowSignal, HolderRow, KseiHolding, OwnershipRelease, OwnershipSource, }; -use crate::ownership::{archive, db, entities, graph, parser, remote, search, snapshot, xlsx}; +use idx_core::ownership::{archive, db, entities, graph, parser, remote, search, snapshot, xlsx}; #[derive(Debug, Args)] #[command( @@ -215,7 +215,7 @@ pub fn handle(cmd: &OwnershipCommand, config: &IdxConfig) -> Result<(), IdxError fn handle_sync(args: &SyncArgs, config: &IdxConfig) -> Result<(), IdxError> { let manifest_source = snapshot::resolve_manifest_source(args.manifest.as_deref())?; - let db_path = db::db_path(config)?; + let db_path = db::db_path()?; let result = snapshot::sync_snapshot(&manifest_source, &db_path, args.force)?; if config.output.is_json() { @@ -301,7 +301,7 @@ fn handle_discover(args: &DiscoverArgs, config: &IdxConfig) -> Result<(), IdxErr } fn handle_ticker(args: &TickerArgs, config: &IdxConfig) -> Result<(), IdxError> { - let conn = db::open_db(config)?; + let conn = db::open_db()?; let symbol = args.symbol.trim().to_uppercase(); let source = args.source.trim().to_lowercase(); if !["all", "ksei", "bing"].contains(&source.as_str()) { @@ -368,8 +368,8 @@ fn handle_ticker(args: &TickerArgs, config: &IdxConfig) -> Result<(), IdxError> Cell::new(&h.name), Cell::new(h.investor_type.clone().unwrap_or_else(|| "-".to_string())), Cell::new(match h.locality { - Some(crate::ownership::types::Locality::Local) => "L", - Some(crate::ownership::types::Locality::Foreign) => "F", + Some(idx_core::ownership::types::Locality::Local) => "L", + Some(idx_core::ownership::types::Locality::Foreign) => "F", None => "-", }), Cell::new(format_idr(h.shares)), @@ -393,7 +393,7 @@ fn handle_ticker(args: &TickerArgs, config: &IdxConfig) -> Result<(), IdxError> } fn handle_entity(args: &EntityArgs, config: &IdxConfig) -> Result<(), IdxError> { - let conn = db::open_db(config)?; + let conn = db::open_db()?; let q = format!("%{}%", args.name.trim()); let entity = conn @@ -436,7 +436,7 @@ fn handle_entity(args: &EntityArgs, config: &IdxConfig) -> Result<(), IdxError> } fn handle_search(args: &SearchArgs, config: &IdxConfig) -> Result<(), IdxError> { - let conn = db::open_db(config)?; + let conn = db::open_db()?; let entities = search::fts_search(&conn, &args.query, args.limit)?; if config.output.is_json() { @@ -471,7 +471,7 @@ fn handle_search(args: &SearchArgs, config: &IdxConfig) -> Result<(), IdxError> } fn handle_cross_holders(args: &CrossHolderArgs, config: &IdxConfig) -> Result<(), IdxError> { - let conn = db::open_db(config)?; + let conn = db::open_db()?; let rows = db::query_cross_holders(&conn, args.min_tickers, args.top)?; if config.output.is_json() { @@ -498,7 +498,7 @@ fn handle_cross_holders(args: &CrossHolderArgs, config: &IdxConfig) -> Result<() } fn handle_concentration(args: &ConcentrationArgs, config: &IdxConfig) -> Result<(), IdxError> { - let conn = db::open_db(config)?; + let conn = db::open_db()?; let by = args.by.trim().to_lowercase(); if !["top1", "top3", "hhi"].contains(&by.as_str()) { return Err(IdxError::ParseError( @@ -543,7 +543,7 @@ fn handle_concentration(args: &ConcentrationArgs, config: &IdxConfig) -> Result< } fn handle_flow(args: &FlowArgs, config: &IdxConfig) -> Result<(), IdxError> { - let conn = db::open_db(config)?; + let conn = db::open_db()?; let symbol = args.symbol.trim().to_uppercase(); let ticker_id = db::get_ticker_id(&conn, &symbol)? .ok_or_else(|| IdxError::SymbolNotFound(symbol.clone()))?; @@ -551,7 +551,9 @@ fn handle_flow(args: &FlowArgs, config: &IdxConfig) -> Result<(), IdxError> { let flow = db::query_bing_flow(&conn, ticker_id)?; let Some(flow) = flow else { if config.output.is_json() { - return json::print_json(&Option::::None); + return json::print_json( + &Option::::None, + ); } println!("No institutional flow data available for {symbol}."); return Ok(()); @@ -595,14 +597,14 @@ fn print_flow_section(title: &str, rows: &[HolderRow]) { } fn handle_graph(args: &GraphArgs, config: &IdxConfig) -> Result<(), IdxError> { - let conn = db::open_db(config)?; + let conn = db::open_db()?; let (nodes, edges) = graph::query_ownership_graph(&conn, &args.root, args.depth)?; if config.output.is_json() { #[derive(Serialize)] struct GraphOutput<'a> { - nodes: &'a [crate::ownership::types::GraphNode], - edges: &'a [crate::ownership::types::GraphEdge], + nodes: &'a [idx_core::ownership::types::GraphNode], + edges: &'a [idx_core::ownership::types::GraphEdge], } return json::print_json(&GraphOutput { nodes: &nodes, @@ -626,7 +628,7 @@ fn handle_graph(args: &GraphArgs, config: &IdxConfig) -> Result<(), IdxError> { } fn handle_changes(args: &ChangesArgs, config: &IdxConfig) -> Result<(), IdxError> { - let conn = db::open_db(config)?; + let conn = db::open_db()?; let changes = db::query_changes(&conn, &args.from, &args.to)?; if config.output.is_json() { @@ -672,7 +674,7 @@ fn handle_changes(args: &ChangesArgs, config: &IdxConfig) -> Result<(), IdxError } fn handle_resolve(args: &ResolveArgs, config: &IdxConfig) -> Result<(), IdxError> { - let conn = db::open_db(config)?; + let conn = db::open_db()?; match &args.command { ResolveCommand::Unresolved { limit } => { @@ -799,7 +801,7 @@ fn handle_import(args: &ImportArgs, config: &IdxConfig) -> Result<(), IdxError> return Ok(()); }; - let conn = db::open_db(config)?; + let conn = db::open_db()?; let sha256 = sha256_file(&import_input.import_path)?; if !args.force && db::release_exists(&conn, &sha256)? { @@ -911,7 +913,7 @@ fn handle_import(args: &ImportArgs, config: &IdxConfig) -> Result<(), IdxError> } fn handle_releases(config: &IdxConfig) -> Result<(), IdxError> { - let conn = db::open_db(config)?; + let conn = db::open_db()?; let releases = db::query_releases(&conn)?; if config.output.is_json() { @@ -1042,7 +1044,7 @@ fn cache_download_path(url: &str, format: ImportInputFormat) -> Result ".xlsx", _ => ".pdf", }; - let raw_dir = crate::cache::cache_dir()?.join("ownership").join("raw"); + let raw_dir = idx_core::cache::cache_dir()?.join("ownership").join("raw"); fs::create_dir_all(&raw_dir).map_err(|e| IdxError::Io(e.to_string()))?; let mut file_name = url diff --git a/src/cli/stocks.rs b/crates/idx-cli/src/cli/stocks.rs similarity index 95% rename from src/cli/stocks.rs rename to crates/idx-cli/src/cli/stocks.rs index 430c1ce..8aaae85 100644 --- a/src/cli/stocks.rs +++ b/crates/idx-cli/src/cli/stocks.rs @@ -4,27 +4,27 @@ use std::io::Read; use clap::{Args, Subcommand, ValueEnum}; use serde::{Serialize, de::DeserializeOwned}; -use crate::analysis::fundamental::{ - FundamentalReport, GrowthReport, RiskReport, ValuationReport, analyze_fundamental, - analyze_growth, analyze_risk, analyze_valuation, -}; -use crate::analysis::signals::{self, Signal, TechnicalSignal}; -use crate::analysis::technical; -use crate::api::types::{ - CompanyProfile, EarningsReport, FinancialStatements, Fundamentals, InsightData, Interval, - NewsItem, Ohlc, Period, Quote, SentimentData, -}; -use crate::api::{MarketDataProvider, SelectedProvider, history_provider}; -use crate::cache::Cache; use crate::config::{HistoryProviderKind, IdxConfig, ProviderKind}; -use crate::error::IdxError; use crate::output::{ MacdSnapshot, TechnicalReport, VolumeSnapshot, render_compare, render_earnings, render_financials, render_fundamental, render_growth, render_history, render_insights, render_news, render_profile, render_quotes, render_risk, render_screener, render_sentiment, render_technical, render_valuation, }; -use crate::runtime; +use idx_core::analysis::fundamental::{ + FundamentalReport, GrowthReport, RiskReport, ValuationReport, analyze_fundamental, + analyze_growth, analyze_risk, analyze_valuation, +}; +use idx_core::analysis::signals::{self, Signal, TechnicalSignal}; +use idx_core::analysis::technical; +use idx_core::api::types::{ + CompanyProfile, EarningsReport, FinancialStatements, Fundamentals, InsightData, Interval, + NewsItem, Ohlc, Period, Quote, SentimentData, +}; +use idx_core::api::{MarketDataProvider, SelectedProvider, history_provider}; +use idx_core::cache::Cache; +use idx_core::error::IdxError; +use idx_core::runtime; struct FundamentalCacheSpec { bucket: String, @@ -287,7 +287,7 @@ pub fn handle( let quote_bucket = cache_bucket(provider.kind(), "quote"); let mut quotes = Vec::new(); for sym in expand_symbols(symbols)? { - let resolved = crate::api::resolve_symbol(&sym, &config.exchange)?; + let resolved = idx_core::api::resolve_symbol(&sym, &config.exchange)?; if !no_cache && let Some(q) = cache.get("e_bucket, &resolved)? { quotes.push(q); continue; @@ -343,10 +343,10 @@ pub fn handle( )); } let history_bucket = format!("{}-history", history_source.as_str()); - let resolved = crate::api::resolve_symbol(symbol, &config.exchange)?; + let resolved = idx_core::api::resolve_symbol(symbol, &config.exchange)?; let key = format!("{}-{}", period.as_str(), interval.as_str()); if !no_cache - && let Some(history) = cache.get::>( + && let Some(history) = cache.get::>( &history_bucket, &format!("{resolved}-{key}"), )? @@ -355,7 +355,7 @@ pub fn handle( } if offline { let stale = cache - .get_offline::>( + .get_offline::>( &history_bucket, &format!("{resolved}-{key}"), )? @@ -379,7 +379,7 @@ pub fn handle( } Err(err) => { if !no_cache - && let Some(stale) = cache.get_stale::>( + && let Some(stale) = cache.get_stale::>( &history_bucket, &format!("{resolved}-{key}"), )? @@ -411,7 +411,7 @@ pub fn handle( )); } let technical_bucket = format!("{}-technical", history_source.as_str()); - let resolved = crate::api::resolve_symbol(symbol, &config.exchange)?; + let resolved = idx_core::api::resolve_symbol(symbol, &config.exchange)?; if !no_cache && let Some(report) = cache.get::(&technical_bucket, &resolved)? { @@ -447,7 +447,7 @@ pub fn handle( } } StocksSubcommand::Growth { symbol } => { - let resolved = crate::api::resolve_symbol(symbol, &config.exchange)?; + let resolved = idx_core::api::resolve_symbol(symbol, &config.exchange)?; let report: GrowthReport = fetch_fundamental_analysis_report( &cache, provider.market(), @@ -463,7 +463,7 @@ pub fn handle( render_growth(&resolved, &report, &config.output, config.no_color) } StocksSubcommand::Valuation { symbol } => { - let resolved = crate::api::resolve_symbol(symbol, &config.exchange)?; + let resolved = idx_core::api::resolve_symbol(symbol, &config.exchange)?; let report: ValuationReport = fetch_fundamental_analysis_report( &cache, provider.market(), @@ -479,7 +479,7 @@ pub fn handle( render_valuation(&resolved, &report, &config.output, config.no_color) } StocksSubcommand::Risk { symbol } => { - let resolved = crate::api::resolve_symbol(symbol, &config.exchange)?; + let resolved = idx_core::api::resolve_symbol(symbol, &config.exchange)?; let report: RiskReport = fetch_fundamental_analysis_report( &cache, provider.market(), @@ -495,7 +495,7 @@ pub fn handle( render_risk(&resolved, &report, &config.output, config.no_color) } StocksSubcommand::Fundamental { symbol } => { - let resolved = crate::api::resolve_symbol(symbol, &config.exchange)?; + let resolved = idx_core::api::resolve_symbol(symbol, &config.exchange)?; let report: FundamentalReport = fetch_fundamental_analysis_report( &cache, provider.market(), @@ -511,7 +511,7 @@ pub fn handle( render_fundamental(&report, &config.output, config.no_color) } StocksSubcommand::Profile { symbol } => { - let resolved = crate::api::resolve_symbol(symbol, &config.exchange)?; + let resolved = idx_core::api::resolve_symbol(symbol, &config.exchange)?; let bucket = cache_bucket(provider.kind(), "profile-v2"); let profile_provider = provider.profile_provider(&resolved)?; let profile: CompanyProfile = fetch_with_cache( @@ -529,7 +529,7 @@ pub fn handle( render_profile(&profile, &config.output) } StocksSubcommand::Financials { symbol, filters } => { - let resolved = crate::api::resolve_symbol(symbol, &config.exchange)?; + let resolved = idx_core::api::resolve_symbol(symbol, &config.exchange)?; let bucket = cache_bucket(provider.kind(), "financials"); let financials_provider = provider.financials_provider(&resolved)?; let mut financials: FinancialStatements = fetch_with_cache( @@ -553,7 +553,7 @@ pub fn handle( render_financials(&filtered, &config.output) } StocksSubcommand::Earnings { symbol, filters } => { - let resolved = crate::api::resolve_symbol(symbol, &config.exchange)?; + let resolved = idx_core::api::resolve_symbol(symbol, &config.exchange)?; let bucket = cache_bucket(provider.kind(), "earnings"); let earnings_provider = provider.earnings_provider(&resolved)?; let mut earnings: EarningsReport = fetch_with_cache( @@ -575,7 +575,7 @@ pub fn handle( render_earnings(&filtered, &config.output) } StocksSubcommand::Sentiment { symbol } => { - let resolved = crate::api::resolve_symbol(symbol, &config.exchange)?; + let resolved = idx_core::api::resolve_symbol(symbol, &config.exchange)?; let bucket = cache_bucket(provider.kind(), "sentiment"); let sentiment_provider = provider.sentiment_provider(&resolved)?; let sentiment: SentimentData = fetch_with_cache( @@ -593,7 +593,7 @@ pub fn handle( render_sentiment(&sentiment, &config.output) } StocksSubcommand::Insights { symbol } => { - let resolved = crate::api::resolve_symbol(symbol, &config.exchange)?; + let resolved = idx_core::api::resolve_symbol(symbol, &config.exchange)?; let bucket = cache_bucket(provider.kind(), "insights-v2"); let insights_provider = provider.insights_provider(&resolved)?; let mut insights: InsightData = fetch_with_cache( @@ -614,7 +614,7 @@ pub fn handle( render_insights(&insights, &config.output) } StocksSubcommand::News { symbol, limit } => { - let resolved = crate::api::resolve_symbol(symbol, &config.exchange)?; + let resolved = idx_core::api::resolve_symbol(symbol, &config.exchange)?; let bucket = cache_bucket(provider.kind(), "news"); let key = format!("{resolved}-{limit}"); let news_provider = provider.news_provider(&resolved)?; @@ -679,7 +679,7 @@ pub fn handle( let mut failures = Vec::new(); for sym in expand_symbols(symbols)? { - let resolved = crate::api::resolve_symbol(&sym, &config.exchange)?; + let resolved = idx_core::api::resolve_symbol(&sym, &config.exchange)?; match fetch_fundamental_analysis_report( &cache, provider.market(), @@ -920,7 +920,7 @@ fn filter_financial_statements( } fn filter_earnings_report(report: &EarningsReport, filters: &EarningsFilterArgs) -> EarningsReport { - let filter_rows = |rows: &[crate::api::types::EarningsData]| { + let filter_rows = |rows: &[idx_core::api::types::EarningsData]| { rows.iter() .filter(|row| filters.includes_period(&row.period_type)) .cloned() @@ -1044,11 +1044,11 @@ mod tests { build_technical_report, classify_earnings_period, filter_earnings_report, filter_financial_statements, }; - use crate::analysis::signals::Signal; - use crate::api::types::{ + use idx_core::analysis::signals::Signal; + use idx_core::api::types::{ EarningsData, EarningsReport, FinancialStatements, InstrumentInfo, Ohlc, StatementSection, }; - use crate::error::IdxError; + use idx_core::error::IdxError; #[test] fn technical_report_uses_latest_values() { @@ -1080,7 +1080,7 @@ mod tests { #[test] fn sort_screener_top_performers_descending() { - use crate::api::types::Quote; + use idx_core::api::types::Quote; let mut quotes = vec![ Quote { @@ -1137,7 +1137,7 @@ mod tests { #[test] fn sort_screener_worst_performers_ascending() { - use crate::api::types::Quote; + use idx_core::api::types::Quote; let mut quotes = vec![ Quote { @@ -1205,8 +1205,8 @@ mod tests { assert_eq!(quotes.last().unwrap().symbol, "U"); } - fn sample_quote(symbol: &str, change_pct: Option) -> crate::api::types::Quote { - crate::api::types::Quote { + fn sample_quote(symbol: &str, change_pct: Option) -> idx_core::api::types::Quote { + idx_core::api::types::Quote { symbol: symbol.into(), price: 100, change: None, diff --git a/crates/idx-cli/src/config.rs b/crates/idx-cli/src/config.rs new file mode 100644 index 0000000..c41c1d1 --- /dev/null +++ b/crates/idx-cli/src/config.rs @@ -0,0 +1,167 @@ +use std::fs; +use std::io::IsTerminal; + +use serde::Deserialize; + +pub use idx_core::config::*; + +use crate::cli::Cli; +use crate::output::{OutputFormat, OutputSetting}; +use idx_core::error::IdxError; + +#[derive(Debug, Clone)] +pub struct IdxConfig { + pub provider: ProviderKind, + pub history_provider: HistoryProviderKind, + pub exchange: String, + pub output: OutputFormat, + pub no_color: bool, + pub quote_ttl: u64, + pub fundamental_ttl: u64, +} + +#[derive(Debug, Deserialize, Default)] +struct FileConfig { + general: Option, + cache: Option, +} + +#[derive(Debug, Deserialize, Default)] +struct FileGeneral { + provider: Option, + history_provider: Option, + exchange: Option, + output: Option, + color: Option, +} + +#[derive(Debug, Deserialize, Default)] +struct FileCache { + quote_ttl: Option, + fundamental_ttl: Option, +} + +impl Default for IdxConfig { + fn default() -> Self { + Self { + provider: ProviderKind::Msn, + history_provider: HistoryProviderKind::Auto, + exchange: "JK".to_string(), + output: OutputSetting::Auto.resolve(stdout_is_terminal()), + no_color: false, + quote_ttl: 300, + fundamental_ttl: 3600, + } + } +} + +impl IdxConfig { + pub fn load_with_cli(cli: &Cli) -> Result { + let mut cfg = Self::load()?; + + if let Ok(provider) = std::env::var("IDX_PROVIDER") { + cfg.provider = ProviderKind::parse(&provider)?; + } + if let Ok(history_provider) = std::env::var("IDX_HISTORY_PROVIDER") { + cfg.history_provider = HistoryProviderKind::parse(&history_provider)?; + } + if let Ok(exchange) = std::env::var("IDX_EXCHANGE") { + cfg.exchange = exchange; + } + if let Ok(output) = std::env::var("IDX_OUTPUT") { + let setting = OutputSetting::parse(&output).ok_or_else(|| { + IdxError::ConfigError(format!( + "invalid IDX_OUTPUT value: '{output}', expected 'auto', 'table', 'json', or 'jsonl'" + )) + })?; + cfg.output = setting.resolve(stdout_is_terminal()); + } + if let Ok(no_color) = std::env::var("IDX_NO_COLOR") { + cfg.no_color = no_color == "1" || no_color.eq_ignore_ascii_case("true"); + } + if let Some(parsed) = parse_env_ttl("IDX_CACHE_QUOTE_TTL")? { + cfg.quote_ttl = parsed; + } + if let Some(parsed) = parse_env_ttl("IDX_CACHE_FUNDAMENTAL_TTL")? { + cfg.fundamental_ttl = parsed; + } + + if let Some(output) = cli.output { + cfg.output = output.resolve(stdout_is_terminal()); + } + cfg.no_color = cfg.no_color || cli.no_color || !stdout_is_terminal(); + + Ok(cfg) + } + + pub fn load() -> Result { + let mut cfg = Self::default(); + let path = config_path()?; + if path.exists() { + let raw = fs::read_to_string(&path).map_err(|e| IdxError::Io(e.to_string()))?; + let parsed: FileConfig = + toml::from_str(&raw).map_err(|e| IdxError::ConfigError(e.to_string()))?; + if let Some(general) = parsed.general { + if let Some(provider) = general.provider { + cfg.provider = provider; + } + if let Some(history_provider) = general.history_provider { + cfg.history_provider = history_provider; + } + if let Some(exchange) = general.exchange { + cfg.exchange = exchange; + } + if let Some(output) = general.output { + cfg.output = output.resolve(stdout_is_terminal()); + } + if let Some(color) = general.color { + cfg.no_color = !color; + } + } + if let Some(cache) = parsed.cache { + if let Some(v) = cache.quote_ttl { + cfg.quote_ttl = v; + } + if let Some(v) = cache.fundamental_ttl { + cfg.fundamental_ttl = v; + } + } + } + Ok(cfg) + } +} + +pub fn stdout_is_terminal() -> bool { + std::io::stdout().is_terminal() +} + +#[cfg(test)] +mod tests { + use super::IdxConfig; + + #[test] + fn default_values_are_sane() { + let cfg = IdxConfig::default(); + assert_eq!(cfg.provider, super::ProviderKind::Msn); + assert_eq!(cfg.history_provider, super::HistoryProviderKind::Auto); + assert_eq!(cfg.exchange, "JK"); + assert_eq!(cfg.quote_ttl, 300); + } +} + +#[cfg(test)] +mod output_values_tests { + use clap::ValueEnum; + + use crate::output::OutputSetting; + + #[test] + fn core_output_values_match_cli_output_setting() { + let cli: Vec = OutputSetting::value_variants() + .iter() + .filter_map(|v| v.to_possible_value()) + .map(|v| v.get_name().to_string()) + .collect(); + assert_eq!(cli, super::OUTPUT_VALUES); + } +} diff --git a/src/main.rs b/crates/idx-cli/src/main.rs similarity index 80% rename from src/main.rs rename to crates/idx-cli/src/main.rs index 0d38ea5..4c75dd0 100644 --- a/src/main.rs +++ b/crates/idx-cli/src/main.rs @@ -1,50 +1,18 @@ -pub mod analysis; -mod api; -mod cache; mod cli; mod config; -mod curl_impersonate; -mod error; mod output; -#[cfg(feature = "ownership")] -pub mod ownership; -pub mod runtime { - use std::fmt::Display; - use std::sync::atomic::{AtomicBool, Ordering}; - - static QUIET: AtomicBool = AtomicBool::new(false); - - pub fn set_quiet(quiet: bool) { - QUIET.store(quiet, Ordering::Relaxed); - } - - pub fn is_quiet() -> bool { - QUIET.load(Ordering::Relaxed) - } - - pub fn info(message: impl Display) { - if !is_quiet() { - eprintln!("info: {message}"); - } - } - - pub fn warn(message: impl Display) { - if !is_quiet() { - eprintln!("warning: {message}"); - } - } -} +use idx_core::runtime; use clap::CommandFactory; use clap::Parser; use clap_complete::{generate, shells}; -use crate::api::default_provider; use crate::cli::{Cli, Commands}; use crate::config::IdxConfig; -use crate::error::IdxError; use crate::output::{OutputFormat, OutputSetting, emit_error}; +use idx_core::api::default_provider; +use idx_core::error::IdxError; fn main() { if let Err(err) = run() { diff --git a/src/output/json.rs b/crates/idx-cli/src/output/json.rs similarity index 97% rename from src/output/json.rs rename to crates/idx-cli/src/output/json.rs index 1b8d223..9dd7115 100644 --- a/src/output/json.rs +++ b/crates/idx-cli/src/output/json.rs @@ -2,7 +2,7 @@ use std::sync::atomic::{AtomicU8, Ordering}; use serde::Serialize; -use crate::error::IdxError; +use idx_core::error::IdxError; const PRETTY: u8 = 0; const COMPACT: u8 = 1; diff --git a/src/output/mod.rs b/crates/idx-cli/src/output/mod.rs similarity index 97% rename from src/output/mod.rs rename to crates/idx-cli/src/output/mod.rs index 4559a2a..5c43bda 100644 --- a/src/output/mod.rs +++ b/crates/idx-cli/src/output/mod.rs @@ -8,13 +8,15 @@ use chrono::NaiveDate; use clap::ValueEnum; use serde::{Deserialize, Serialize}; -use crate::analysis::fundamental::{FundamentalReport, GrowthReport, RiskReport, ValuationReport}; -use crate::analysis::signals::TechnicalSignal; -use crate::api::types::{ +use idx_core::analysis::fundamental::{ + FundamentalReport, GrowthReport, RiskReport, ValuationReport, +}; +use idx_core::analysis::signals::TechnicalSignal; +use idx_core::api::types::{ CompanyProfile, EarningsReport, FinancialStatements, InsightData, NewsItem, Ohlc, Quote, SentimentData, }; -use crate::error::IdxError; +use idx_core::error::IdxError; /// Output format requested by the user (flag, `IDX_OUTPUT`, or config file). #[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq, Serialize, serde::Deserialize, Default)] diff --git a/src/output/style.rs b/crates/idx-cli/src/output/style.rs similarity index 100% rename from src/output/style.rs rename to crates/idx-cli/src/output/style.rs diff --git a/src/output/table.rs b/crates/idx-cli/src/output/table.rs similarity index 98% rename from src/output/table.rs rename to crates/idx-cli/src/output/table.rs index 2f26f94..f17f8d5 100644 --- a/src/output/table.rs +++ b/crates/idx-cli/src/output/table.rs @@ -1,14 +1,16 @@ use crate::output::style::Paint; use comfy_table::{Cell, Color, ContentArrangement, Table, presets::UTF8_FULL}; -use crate::analysis::fundamental::{FundamentalReport, GrowthReport, RiskReport, ValuationReport}; -use crate::analysis::signals::Signal; -use crate::api::types::{ +use crate::output::TechnicalReport; +use idx_core::analysis::fundamental::{ + FundamentalReport, GrowthReport, RiskReport, ValuationReport, +}; +use idx_core::analysis::signals::Signal; +use idx_core::api::types::{ CompanyProfile, EarningsData, EarningsReport, FinancialStatements, InsightData, NewsItem, Ohlc, Quote, SentimentData, }; -use crate::error::IdxError; -use crate::output::TechnicalReport; +use idx_core::error::IdxError; pub fn format_idr(value: i64) -> String { let sign = if value.is_negative() { "-" } else { "" }; @@ -708,7 +710,7 @@ pub fn print_profile(profile: &CompanyProfile) -> Result<(), IdxError> { } pub fn print_financials(fin: &FinancialStatements) -> Result<(), IdxError> { - let print_section = |label: &str, section: &crate::api::types::StatementSection| { + let print_section = |label: &str, section: &idx_core::api::types::StatementSection| { println!("\n── {label} ({}) ──", format_table_date(§ion.end_date)); let mut t = Table::new(); let value_header = format!("VALUE ({})", section.currency); @@ -887,8 +889,8 @@ mod tests { format_earnings_period, format_idr, format_signal, format_table_date, format_u64, humanize_metric_key, stale_quote_notes, }; - use crate::analysis::signals::Signal; - use crate::api::types::Quote; + use idx_core::analysis::signals::Signal; + use idx_core::api::types::Quote; fn quote_as_of(symbol: &str, as_of: &str) -> Quote { Quote { diff --git a/tests/cli.rs b/crates/idx-cli/tests/cli.rs similarity index 97% rename from tests/cli.rs rename to crates/idx-cli/tests/cli.rs index 5e3f34d..9baa2f0 100644 --- a/tests/cli.rs +++ b/crates/idx-cli/tests/cli.rs @@ -26,7 +26,7 @@ fn resolve_idx_binary_path() -> PathBuf { } let target_debug = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("target") + .join("../../target") .join("debug") .join(format!("idx{}", std::env::consts::EXE_SUFFIX)); if candidate_is_idx_binary(&target_debug) { @@ -113,7 +113,8 @@ fn bin_with_root(root: &Path) -> Command { fs::create_dir_all(&cache_home).expect("create cache dir"); let mut cmd = bin(); - cmd.current_dir(env!("CARGO_MANIFEST_DIR")); + // The mock provider reads fixtures relative to the working directory. + cmd.current_dir(core_dir()); // Tests run with piped stdout, where the default (auto) format is JSON. // Pin table so tests that assert table text keep doing so; `-o` and // per-test IDX_OUTPUT still override this. @@ -128,11 +129,13 @@ fn test_bin(name: &str) -> Command { bin_with_root(&root) } +/// idx-core crate root, which owns the shared test fixtures. +fn core_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../idx-core") +} + fn fixture_path(name: &str) -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .join("tests") - .join("fixtures") - .join(name) + core_dir().join("tests").join("fixtures").join(name) } fn run_success_stdout(cmd: &mut Command) -> String { @@ -375,11 +378,11 @@ fn prepare_snapshot_fixture(root: &Path) -> (PathBuf, PathBuf) { &[ ( "release-2026-01-31.pdf", - include_str!("fixtures/ksei_above1_stext_excerpt_prev.xml"), + include_str!("../../idx-core/tests/fixtures/ksei_above1_stext_excerpt_prev.xml"), ), ( "release-2026-02-27.pdf", - include_str!("fixtures/ksei_above1_stext_excerpt.xml"), + include_str!("../../idx-core/tests/fixtures/ksei_above1_stext_excerpt.xml"), ), ], ); @@ -1064,7 +1067,7 @@ const UNREACHABLE_DATA_PAGE_URL: &str = "http://127.0.0.1:9/data-kepemilikan-sah #[test] fn ownership_discover_finds_xlsx_on_share_ownership_page() { - let page = fs::read_to_string("tests/fixtures/idx_share_ownership_page_excerpt.html") + let page = fs::read_to_string(fixture_path("idx_share_ownership_page_excerpt.html")) .expect("read share ownership page fixture"); let page_url = spawn_single_response_server("text/html", page); @@ -1098,7 +1101,7 @@ fn ownership_discover_finds_xlsx_on_share_ownership_page() { #[test] fn ownership_discover_lists_fixture_candidates() { - let body = fs::read_to_string("tests/fixtures/idx_announcement_kepemilikan.json") + let body = fs::read_to_string(fixture_path("idx_announcement_kepemilikan.json")) .expect("read ownership discovery fixture"); let json_url = spawn_single_response_server("application/json", body); @@ -1332,7 +1335,7 @@ fn ownership_import_url_supported_pdf_succeeds_with_fake_mutool() { let db_path = root.join("ownership.db"); let fake_mutool_dir = install_fake_mutool( &root, - include_str!("fixtures/ksei_above1_stext_excerpt.xml"), + include_str!("../../idx-core/tests/fixtures/ksei_above1_stext_excerpt.xml"), ); let pdf_base = spawn_single_response_server("application/pdf", fake_pdf_bytes()); let pdf_url = pdf_url(&pdf_base, "supported"); @@ -1369,7 +1372,7 @@ fn ownership_import_url_caches_download_under_xdg_cache_home() { let cache_home = root.join("xdg-cache"); let fake_mutool_dir = install_fake_mutool( &root, - include_str!("fixtures/ksei_above1_stext_excerpt.xml"), + include_str!("../../idx-core/tests/fixtures/ksei_above1_stext_excerpt.xml"), ); let pdf_base = spawn_single_response_server("application/pdf", fake_pdf_bytes()); let pdf_url = pdf_url(&pdf_base, "cached-raw"); @@ -1408,7 +1411,7 @@ fn ownership_import_url_duplicate_release_is_skipped() { let db_path = root.join("ownership.db"); let fake_mutool_dir = install_fake_mutool( &root, - include_str!("fixtures/ksei_above1_stext_excerpt.xml"), + include_str!("../../idx-core/tests/fixtures/ksei_above1_stext_excerpt.xml"), ); let first_base = spawn_single_response_server("application/pdf", fake_pdf_bytes()); let second_base = spawn_single_response_server("application/pdf", fake_pdf_bytes()); @@ -1446,7 +1449,7 @@ fn ownership_import_file_xlsx_supports_ticker_and_releases() { let xlsx_path = root.join("peng-2026-08-00017-satu-persen.xlsx"); fs::write( &xlsx_path, - include_bytes!("fixtures/ksei_above1_20260831_excerpt.xlsx"), + include_bytes!("../../idx-core/tests/fixtures/ksei_above1_20260831_excerpt.xlsx"), ) .expect("write xlsx fixture"); @@ -1495,7 +1498,7 @@ fn ownership_import_file_rejects_above5_xlsx() { let xlsx_path = root.join("peng-2026-09-23-00080-lima-persen.xlsx"); fs::write( &xlsx_path, - include_bytes!("fixtures/ksei_above5_20260923_excerpt.xlsx"), + include_bytes!("../../idx-core/tests/fixtures/ksei_above5_20260923_excerpt.xlsx"), ) .expect("write xlsx fixture"); @@ -1523,7 +1526,7 @@ fn ownership_import_force_reimports_existing_release() { let pdf_path = root.join("force.pdf"); let fake_mutool_dir = install_fake_mutool( &root, - include_str!("fixtures/ksei_above1_stext_excerpt.xml"), + include_str!("../../idx-core/tests/fixtures/ksei_above1_stext_excerpt.xml"), ); fs::write(&pdf_path, fake_pdf_bytes()).expect("write local pdf fixture"); @@ -1571,7 +1574,7 @@ fn ownership_import_url_rejects_legacy_above5_pdf_schema() { let data_home = root.join("data"); let fake_mutool_dir = install_fake_mutool( &root, - include_str!("fixtures/ksei_above5_stext_excerpt.xml"), + include_str!("../../idx-core/tests/fixtures/ksei_above5_stext_excerpt.xml"), ); let pdf_base = spawn_single_response_server("application/pdf", fake_pdf_bytes()); let pdf_url = pdf_url(&pdf_base, "legacy-above5"); @@ -1593,7 +1596,7 @@ fn ownership_import_url_rejects_legacy_investor_type_pdf_schema() { let data_home = root.join("data"); let fake_mutool_dir = install_fake_mutool( &root, - include_str!("fixtures/ksei_investor_type_stext_excerpt.xml"), + include_str!("../../idx-core/tests/fixtures/ksei_investor_type_stext_excerpt.xml"), ); let pdf_base = spawn_single_response_server("application/pdf", fake_pdf_bytes()); let pdf_url = pdf_url(&pdf_base, "legacy-investor-type"); @@ -1616,7 +1619,7 @@ fn ownership_import_file_txt_archive_succeeds() { let txt_path = root.join("Balancepos20260227.txt"); fs::write( &txt_path, - include_str!("fixtures/ksei_balancepos_20260227_excerpt.txt"), + include_str!("../../idx-core/tests/fixtures/ksei_balancepos_20260227_excerpt.txt"), ) .expect("write txt archive fixture"); @@ -1654,12 +1657,12 @@ fn ownership_import_file_zip_archive_supports_releases_ticker_and_changes() { write_zip_with_text( &jan_zip, "Balancepos20260130.txt", - include_str!("fixtures/ksei_balancepos_20260130_excerpt.txt"), + include_str!("../../idx-core/tests/fixtures/ksei_balancepos_20260130_excerpt.txt"), ); write_zip_with_text( &feb_zip, "Balancepos20260227.txt", - include_str!("fixtures/ksei_balancepos_20260227_excerpt.txt"), + include_str!("../../idx-core/tests/fixtures/ksei_balancepos_20260227_excerpt.txt"), ); bin_with_root(&root) diff --git a/tests/golden.rs b/crates/idx-cli/tests/golden.rs similarity index 91% rename from tests/golden.rs rename to crates/idx-cli/tests/golden.rs index d6c5aba..e670b9e 100644 --- a/tests/golden.rs +++ b/crates/idx-cli/tests/golden.rs @@ -18,7 +18,7 @@ use std::sync::OnceLock; use serde::Deserialize; const IDX: &str = env!("CARGO_BIN_EXE_idx"); -const REPO: &str = env!("CARGO_MANIFEST_DIR"); +const CRATE: &str = env!("CARGO_MANIFEST_DIR"); #[derive(Deserialize)] struct CaseFile { @@ -50,8 +50,17 @@ fn default_formats() -> Vec { vec!["table".into(), "json".into()] } +/// Commands run from the idx-core crate root: the mock provider and the cases +/// read fixtures as `tests/fixtures/...` relative to the working directory. +fn core_dir() -> PathBuf { + Path::new(CRATE) + .join("../idx-core") + .canonicalize() + .expect("idx-core crate dir") +} + fn golden_dir() -> PathBuf { - Path::new(REPO).join("tests").join("golden") + Path::new(CRATE).join("tests").join("golden") } fn work_dir() -> PathBuf { @@ -70,7 +79,7 @@ fn idx_with_stdin( stdin: Option<&str>, ) -> std::process::Output { let mut cmd = Command::new(IDX); - cmd.current_dir(REPO).args(args); + cmd.current_dir(core_dir()).args(args); for (key, _) in std::env::vars_os() { if key.to_string_lossy().starts_with("IDX_") || key == "NO_COLOR" { cmd.env_remove(key); @@ -105,14 +114,14 @@ fn seeded_ownership_db() -> &'static Path { // The importer derives the release from the IDX file name. let xlsx = root.join("peng-2026-08-00017-satu-persen.xlsx"); fs::copy( - Path::new(REPO).join("tests/fixtures/ksei_above1_20260831_excerpt.xlsx"), + core_dir().join("tests/fixtures/ksei_above1_20260831_excerpt.xlsx"), &xlsx, ) .expect("copy xlsx fixture"); let inputs = [ xlsx, - Path::new(REPO).join("tests/fixtures/ksei_balancepos_20260130_excerpt.txt"), - Path::new(REPO).join("tests/fixtures/ksei_balancepos_20260227_excerpt.txt"), + core_dir().join("tests/fixtures/ksei_balancepos_20260130_excerpt.txt"), + core_dir().join("tests/fixtures/ksei_balancepos_20260227_excerpt.txt"), ]; for input in inputs { let args = ["ownership", "import", "--file", input.to_str().unwrap()] @@ -137,7 +146,7 @@ fn render(root: &Path, out: &std::process::Output) -> String { let normalize = |bytes: &[u8]| { String::from_utf8_lossy(bytes) .replace(root.to_str().unwrap(), "") - .replace(REPO, "") + .replace(core_dir().to_str().unwrap(), "") }; format!( "exit: {}\n--- stdout\n{}--- stderr\n{}", diff --git a/tests/golden/auto-format-env-table.golden b/crates/idx-cli/tests/golden/auto-format-env-table.golden similarity index 100% rename from tests/golden/auto-format-env-table.golden rename to crates/idx-cli/tests/golden/auto-format-env-table.golden diff --git a/tests/golden/auto-format-piped.golden b/crates/idx-cli/tests/golden/auto-format-piped.golden similarity index 100% rename from tests/golden/auto-format-piped.golden rename to crates/idx-cli/tests/golden/auto-format-piped.golden diff --git a/tests/golden/cache-clear.json.golden b/crates/idx-cli/tests/golden/cache-clear.json.golden similarity index 100% rename from tests/golden/cache-clear.json.golden rename to crates/idx-cli/tests/golden/cache-clear.json.golden diff --git a/tests/golden/cache-clear.table.golden b/crates/idx-cli/tests/golden/cache-clear.table.golden similarity index 100% rename from tests/golden/cache-clear.table.golden rename to crates/idx-cli/tests/golden/cache-clear.table.golden diff --git a/tests/golden/cache-info-empty.json.golden b/crates/idx-cli/tests/golden/cache-info-empty.json.golden similarity index 100% rename from tests/golden/cache-info-empty.json.golden rename to crates/idx-cli/tests/golden/cache-info-empty.json.golden diff --git a/tests/golden/cache-info-empty.table.golden b/crates/idx-cli/tests/golden/cache-info-empty.table.golden similarity index 100% rename from tests/golden/cache-info-empty.table.golden rename to crates/idx-cli/tests/golden/cache-info-empty.table.golden diff --git a/tests/golden/cases.toml b/crates/idx-cli/tests/golden/cases.toml similarity index 100% rename from tests/golden/cases.toml rename to crates/idx-cli/tests/golden/cases.toml diff --git a/tests/golden/completions-bash.golden b/crates/idx-cli/tests/golden/completions-bash.golden similarity index 100% rename from tests/golden/completions-bash.golden rename to crates/idx-cli/tests/golden/completions-bash.golden diff --git a/tests/golden/config-get.golden b/crates/idx-cli/tests/golden/config-get.golden similarity index 100% rename from tests/golden/config-get.golden rename to crates/idx-cli/tests/golden/config-get.golden diff --git a/tests/golden/config-init.golden b/crates/idx-cli/tests/golden/config-init.golden similarity index 100% rename from tests/golden/config-init.golden rename to crates/idx-cli/tests/golden/config-init.golden diff --git a/tests/golden/config-output-table-pins-format.golden b/crates/idx-cli/tests/golden/config-output-table-pins-format.golden similarity index 100% rename from tests/golden/config-output-table-pins-format.golden rename to crates/idx-cli/tests/golden/config-output-table-pins-format.golden diff --git a/tests/golden/config-path.golden b/crates/idx-cli/tests/golden/config-path.golden similarity index 100% rename from tests/golden/config-path.golden rename to crates/idx-cli/tests/golden/config-path.golden diff --git a/tests/golden/config-set-output-invalid.golden b/crates/idx-cli/tests/golden/config-set-output-invalid.golden similarity index 100% rename from tests/golden/config-set-output-invalid.golden rename to crates/idx-cli/tests/golden/config-set-output-invalid.golden diff --git a/tests/golden/config-set-output-jsonl.golden b/crates/idx-cli/tests/golden/config-set-output-jsonl.golden similarity index 100% rename from tests/golden/config-set-output-jsonl.golden rename to crates/idx-cli/tests/golden/config-set-output-jsonl.golden diff --git a/tests/golden/config-set.golden b/crates/idx-cli/tests/golden/config-set.golden similarity index 100% rename from tests/golden/config-set.golden rename to crates/idx-cli/tests/golden/config-set.golden diff --git a/tests/golden/error-compare-total-failure.json.golden b/crates/idx-cli/tests/golden/error-compare-total-failure.json.golden similarity index 100% rename from tests/golden/error-compare-total-failure.json.golden rename to crates/idx-cli/tests/golden/error-compare-total-failure.json.golden diff --git a/tests/golden/error-compare-total-failure.table.golden b/crates/idx-cli/tests/golden/error-compare-total-failure.table.golden similarity index 100% rename from tests/golden/error-compare-total-failure.table.golden rename to crates/idx-cli/tests/golden/error-compare-total-failure.table.golden diff --git a/tests/golden/error-fetch-bing-unsupported.json.golden b/crates/idx-cli/tests/golden/error-fetch-bing-unsupported.json.golden similarity index 100% rename from tests/golden/error-fetch-bing-unsupported.json.golden rename to crates/idx-cli/tests/golden/error-fetch-bing-unsupported.json.golden diff --git a/tests/golden/error-fetch-bing-unsupported.table.golden b/crates/idx-cli/tests/golden/error-fetch-bing-unsupported.table.golden similarity index 100% rename from tests/golden/error-fetch-bing-unsupported.table.golden rename to crates/idx-cli/tests/golden/error-fetch-bing-unsupported.table.golden diff --git a/tests/golden/error-industry-only-fundamentals.json.golden b/crates/idx-cli/tests/golden/error-industry-only-fundamentals.json.golden similarity index 100% rename from tests/golden/error-industry-only-fundamentals.json.golden rename to crates/idx-cli/tests/golden/error-industry-only-fundamentals.json.golden diff --git a/tests/golden/error-industry-only-fundamentals.table.golden b/crates/idx-cli/tests/golden/error-industry-only-fundamentals.table.golden similarity index 100% rename from tests/golden/error-industry-only-fundamentals.table.golden rename to crates/idx-cli/tests/golden/error-industry-only-fundamentals.table.golden diff --git a/tests/golden/error-invalid-output-env.golden b/crates/idx-cli/tests/golden/error-invalid-output-env.golden similarity index 100% rename from tests/golden/error-invalid-output-env.golden rename to crates/idx-cli/tests/golden/error-invalid-output-env.golden diff --git a/tests/golden/error-invalid-provider.json.golden b/crates/idx-cli/tests/golden/error-invalid-provider.json.golden similarity index 100% rename from tests/golden/error-invalid-provider.json.golden rename to crates/idx-cli/tests/golden/error-invalid-provider.json.golden diff --git a/tests/golden/error-invalid-provider.table.golden b/crates/idx-cli/tests/golden/error-invalid-provider.table.golden similarity index 100% rename from tests/golden/error-invalid-provider.table.golden rename to crates/idx-cli/tests/golden/error-invalid-provider.table.golden diff --git a/tests/golden/error-offline-cache-miss.json.golden b/crates/idx-cli/tests/golden/error-offline-cache-miss.json.golden similarity index 100% rename from tests/golden/error-offline-cache-miss.json.golden rename to crates/idx-cli/tests/golden/error-offline-cache-miss.json.golden diff --git a/tests/golden/error-offline-cache-miss.table.golden b/crates/idx-cli/tests/golden/error-offline-cache-miss.table.golden similarity index 100% rename from tests/golden/error-offline-cache-miss.table.golden rename to crates/idx-cli/tests/golden/error-offline-cache-miss.table.golden diff --git a/tests/golden/error-offline-no-cache-flags.json.golden b/crates/idx-cli/tests/golden/error-offline-no-cache-flags.json.golden similarity index 100% rename from tests/golden/error-offline-no-cache-flags.json.golden rename to crates/idx-cli/tests/golden/error-offline-no-cache-flags.json.golden diff --git a/tests/golden/error-offline-no-cache-flags.table.golden b/crates/idx-cli/tests/golden/error-offline-no-cache-flags.table.golden similarity index 100% rename from tests/golden/error-offline-no-cache-flags.table.golden rename to crates/idx-cli/tests/golden/error-offline-no-cache-flags.table.golden diff --git a/tests/golden/error-provider-failure.json.golden b/crates/idx-cli/tests/golden/error-provider-failure.json.golden similarity index 100% rename from tests/golden/error-provider-failure.json.golden rename to crates/idx-cli/tests/golden/error-provider-failure.json.golden diff --git a/tests/golden/error-provider-failure.table.golden b/crates/idx-cli/tests/golden/error-provider-failure.table.golden similarity index 100% rename from tests/golden/error-provider-failure.table.golden rename to crates/idx-cli/tests/golden/error-provider-failure.table.golden diff --git a/tests/golden/error-screen-region.json.golden b/crates/idx-cli/tests/golden/error-screen-region.json.golden similarity index 100% rename from tests/golden/error-screen-region.json.golden rename to crates/idx-cli/tests/golden/error-screen-region.json.golden diff --git a/tests/golden/error-screen-region.jsonl.golden b/crates/idx-cli/tests/golden/error-screen-region.jsonl.golden similarity index 100% rename from tests/golden/error-screen-region.jsonl.golden rename to crates/idx-cli/tests/golden/error-screen-region.jsonl.golden diff --git a/tests/golden/error-screen-region.table.golden b/crates/idx-cli/tests/golden/error-screen-region.table.golden similarity index 100% rename from tests/golden/error-screen-region.table.golden rename to crates/idx-cli/tests/golden/error-screen-region.table.golden diff --git a/tests/golden/error-stdin-empty.json.golden b/crates/idx-cli/tests/golden/error-stdin-empty.json.golden similarity index 100% rename from tests/golden/error-stdin-empty.json.golden rename to crates/idx-cli/tests/golden/error-stdin-empty.json.golden diff --git a/tests/golden/error-stdin-empty.table.golden b/crates/idx-cli/tests/golden/error-stdin-empty.table.golden similarity index 100% rename from tests/golden/error-stdin-empty.table.golden rename to crates/idx-cli/tests/golden/error-stdin-empty.table.golden diff --git a/tests/golden/error-usage-missing-symbol.json.golden b/crates/idx-cli/tests/golden/error-usage-missing-symbol.json.golden similarity index 100% rename from tests/golden/error-usage-missing-symbol.json.golden rename to crates/idx-cli/tests/golden/error-usage-missing-symbol.json.golden diff --git a/tests/golden/error-usage-missing-symbol.table.golden b/crates/idx-cli/tests/golden/error-usage-missing-symbol.table.golden similarity index 100% rename from tests/golden/error-usage-missing-symbol.table.golden rename to crates/idx-cli/tests/golden/error-usage-missing-symbol.table.golden diff --git a/tests/golden/error-yahoo-profile-unsupported.json.golden b/crates/idx-cli/tests/golden/error-yahoo-profile-unsupported.json.golden similarity index 100% rename from tests/golden/error-yahoo-profile-unsupported.json.golden rename to crates/idx-cli/tests/golden/error-yahoo-profile-unsupported.json.golden diff --git a/tests/golden/error-yahoo-profile-unsupported.table.golden b/crates/idx-cli/tests/golden/error-yahoo-profile-unsupported.table.golden similarity index 100% rename from tests/golden/error-yahoo-profile-unsupported.table.golden rename to crates/idx-cli/tests/golden/error-yahoo-profile-unsupported.table.golden diff --git a/tests/golden/help-cache.golden b/crates/idx-cli/tests/golden/help-cache.golden similarity index 100% rename from tests/golden/help-cache.golden rename to crates/idx-cli/tests/golden/help-cache.golden diff --git a/tests/golden/help-config.golden b/crates/idx-cli/tests/golden/help-config.golden similarity index 100% rename from tests/golden/help-config.golden rename to crates/idx-cli/tests/golden/help-config.golden diff --git a/tests/golden/help-ownership.golden b/crates/idx-cli/tests/golden/help-ownership.golden similarity index 100% rename from tests/golden/help-ownership.golden rename to crates/idx-cli/tests/golden/help-ownership.golden diff --git a/tests/golden/help-root.golden b/crates/idx-cli/tests/golden/help-root.golden similarity index 100% rename from tests/golden/help-root.golden rename to crates/idx-cli/tests/golden/help-root.golden diff --git a/tests/golden/help-stocks.golden b/crates/idx-cli/tests/golden/help-stocks.golden similarity index 100% rename from tests/golden/help-stocks.golden rename to crates/idx-cli/tests/golden/help-stocks.golden diff --git a/tests/golden/ownership-changes.json.golden b/crates/idx-cli/tests/golden/ownership-changes.json.golden similarity index 100% rename from tests/golden/ownership-changes.json.golden rename to crates/idx-cli/tests/golden/ownership-changes.json.golden diff --git a/tests/golden/ownership-changes.jsonl.golden b/crates/idx-cli/tests/golden/ownership-changes.jsonl.golden similarity index 100% rename from tests/golden/ownership-changes.jsonl.golden rename to crates/idx-cli/tests/golden/ownership-changes.jsonl.golden diff --git a/tests/golden/ownership-changes.table.golden b/crates/idx-cli/tests/golden/ownership-changes.table.golden similarity index 100% rename from tests/golden/ownership-changes.table.golden rename to crates/idx-cli/tests/golden/ownership-changes.table.golden diff --git a/tests/golden/ownership-concentration.json.golden b/crates/idx-cli/tests/golden/ownership-concentration.json.golden similarity index 100% rename from tests/golden/ownership-concentration.json.golden rename to crates/idx-cli/tests/golden/ownership-concentration.json.golden diff --git a/tests/golden/ownership-concentration.table.golden b/crates/idx-cli/tests/golden/ownership-concentration.table.golden similarity index 100% rename from tests/golden/ownership-concentration.table.golden rename to crates/idx-cli/tests/golden/ownership-concentration.table.golden diff --git a/tests/golden/ownership-cross-holders.json.golden b/crates/idx-cli/tests/golden/ownership-cross-holders.json.golden similarity index 100% rename from tests/golden/ownership-cross-holders.json.golden rename to crates/idx-cli/tests/golden/ownership-cross-holders.json.golden diff --git a/tests/golden/ownership-cross-holders.table.golden b/crates/idx-cli/tests/golden/ownership-cross-holders.table.golden similarity index 100% rename from tests/golden/ownership-cross-holders.table.golden rename to crates/idx-cli/tests/golden/ownership-cross-holders.table.golden diff --git a/tests/golden/ownership-entity.json.golden b/crates/idx-cli/tests/golden/ownership-entity.json.golden similarity index 100% rename from tests/golden/ownership-entity.json.golden rename to crates/idx-cli/tests/golden/ownership-entity.json.golden diff --git a/tests/golden/ownership-entity.table.golden b/crates/idx-cli/tests/golden/ownership-entity.table.golden similarity index 100% rename from tests/golden/ownership-entity.table.golden rename to crates/idx-cli/tests/golden/ownership-entity.table.golden diff --git a/tests/golden/ownership-flow.json.golden b/crates/idx-cli/tests/golden/ownership-flow.json.golden similarity index 100% rename from tests/golden/ownership-flow.json.golden rename to crates/idx-cli/tests/golden/ownership-flow.json.golden diff --git a/tests/golden/ownership-flow.table.golden b/crates/idx-cli/tests/golden/ownership-flow.table.golden similarity index 100% rename from tests/golden/ownership-flow.table.golden rename to crates/idx-cli/tests/golden/ownership-flow.table.golden diff --git a/tests/golden/ownership-graph-dot.json.golden b/crates/idx-cli/tests/golden/ownership-graph-dot.json.golden similarity index 100% rename from tests/golden/ownership-graph-dot.json.golden rename to crates/idx-cli/tests/golden/ownership-graph-dot.json.golden diff --git a/tests/golden/ownership-graph-dot.table.golden b/crates/idx-cli/tests/golden/ownership-graph-dot.table.golden similarity index 100% rename from tests/golden/ownership-graph-dot.table.golden rename to crates/idx-cli/tests/golden/ownership-graph-dot.table.golden diff --git a/tests/golden/ownership-graph.json.golden b/crates/idx-cli/tests/golden/ownership-graph.json.golden similarity index 100% rename from tests/golden/ownership-graph.json.golden rename to crates/idx-cli/tests/golden/ownership-graph.json.golden diff --git a/tests/golden/ownership-graph.table.golden b/crates/idx-cli/tests/golden/ownership-graph.table.golden similarity index 100% rename from tests/golden/ownership-graph.table.golden rename to crates/idx-cli/tests/golden/ownership-graph.table.golden diff --git a/tests/golden/ownership-import-archive-txt.json.golden b/crates/idx-cli/tests/golden/ownership-import-archive-txt.json.golden similarity index 100% rename from tests/golden/ownership-import-archive-txt.json.golden rename to crates/idx-cli/tests/golden/ownership-import-archive-txt.json.golden diff --git a/tests/golden/ownership-import-archive-txt.table.golden b/crates/idx-cli/tests/golden/ownership-import-archive-txt.table.golden similarity index 100% rename from tests/golden/ownership-import-archive-txt.table.golden rename to crates/idx-cli/tests/golden/ownership-import-archive-txt.table.golden diff --git a/tests/golden/ownership-import-xlsx.json.golden b/crates/idx-cli/tests/golden/ownership-import-xlsx.json.golden similarity index 100% rename from tests/golden/ownership-import-xlsx.json.golden rename to crates/idx-cli/tests/golden/ownership-import-xlsx.json.golden diff --git a/tests/golden/ownership-import-xlsx.table.golden b/crates/idx-cli/tests/golden/ownership-import-xlsx.table.golden similarity index 100% rename from tests/golden/ownership-import-xlsx.table.golden rename to crates/idx-cli/tests/golden/ownership-import-xlsx.table.golden diff --git a/tests/golden/ownership-releases-empty.json.golden b/crates/idx-cli/tests/golden/ownership-releases-empty.json.golden similarity index 100% rename from tests/golden/ownership-releases-empty.json.golden rename to crates/idx-cli/tests/golden/ownership-releases-empty.json.golden diff --git a/tests/golden/ownership-releases-empty.table.golden b/crates/idx-cli/tests/golden/ownership-releases-empty.table.golden similarity index 100% rename from tests/golden/ownership-releases-empty.table.golden rename to crates/idx-cli/tests/golden/ownership-releases-empty.table.golden diff --git a/tests/golden/ownership-releases.json.golden b/crates/idx-cli/tests/golden/ownership-releases.json.golden similarity index 100% rename from tests/golden/ownership-releases.json.golden rename to crates/idx-cli/tests/golden/ownership-releases.json.golden diff --git a/tests/golden/ownership-releases.table.golden b/crates/idx-cli/tests/golden/ownership-releases.table.golden similarity index 100% rename from tests/golden/ownership-releases.table.golden rename to crates/idx-cli/tests/golden/ownership-releases.table.golden diff --git a/tests/golden/ownership-resolve-map.json.golden b/crates/idx-cli/tests/golden/ownership-resolve-map.json.golden similarity index 100% rename from tests/golden/ownership-resolve-map.json.golden rename to crates/idx-cli/tests/golden/ownership-resolve-map.json.golden diff --git a/tests/golden/ownership-resolve-map.table.golden b/crates/idx-cli/tests/golden/ownership-resolve-map.table.golden similarity index 100% rename from tests/golden/ownership-resolve-map.table.golden rename to crates/idx-cli/tests/golden/ownership-resolve-map.table.golden diff --git a/tests/golden/ownership-resolve-merge.json.golden b/crates/idx-cli/tests/golden/ownership-resolve-merge.json.golden similarity index 100% rename from tests/golden/ownership-resolve-merge.json.golden rename to crates/idx-cli/tests/golden/ownership-resolve-merge.json.golden diff --git a/tests/golden/ownership-resolve-merge.table.golden b/crates/idx-cli/tests/golden/ownership-resolve-merge.table.golden similarity index 100% rename from tests/golden/ownership-resolve-merge.table.golden rename to crates/idx-cli/tests/golden/ownership-resolve-merge.table.golden diff --git a/tests/golden/ownership-resolve-unresolved.json.golden b/crates/idx-cli/tests/golden/ownership-resolve-unresolved.json.golden similarity index 100% rename from tests/golden/ownership-resolve-unresolved.json.golden rename to crates/idx-cli/tests/golden/ownership-resolve-unresolved.json.golden diff --git a/tests/golden/ownership-resolve-unresolved.table.golden b/crates/idx-cli/tests/golden/ownership-resolve-unresolved.table.golden similarity index 100% rename from tests/golden/ownership-resolve-unresolved.table.golden rename to crates/idx-cli/tests/golden/ownership-resolve-unresolved.table.golden diff --git a/tests/golden/ownership-search.json.golden b/crates/idx-cli/tests/golden/ownership-search.json.golden similarity index 100% rename from tests/golden/ownership-search.json.golden rename to crates/idx-cli/tests/golden/ownership-search.json.golden diff --git a/tests/golden/ownership-search.table.golden b/crates/idx-cli/tests/golden/ownership-search.table.golden similarity index 100% rename from tests/golden/ownership-search.table.golden rename to crates/idx-cli/tests/golden/ownership-search.table.golden diff --git a/tests/golden/ownership-ticker-empty.json.golden b/crates/idx-cli/tests/golden/ownership-ticker-empty.json.golden similarity index 100% rename from tests/golden/ownership-ticker-empty.json.golden rename to crates/idx-cli/tests/golden/ownership-ticker-empty.json.golden diff --git a/tests/golden/ownership-ticker-empty.table.golden b/crates/idx-cli/tests/golden/ownership-ticker-empty.table.golden similarity index 100% rename from tests/golden/ownership-ticker-empty.table.golden rename to crates/idx-cli/tests/golden/ownership-ticker-empty.table.golden diff --git a/tests/golden/ownership-ticker-ksei.json.golden b/crates/idx-cli/tests/golden/ownership-ticker-ksei.json.golden similarity index 100% rename from tests/golden/ownership-ticker-ksei.json.golden rename to crates/idx-cli/tests/golden/ownership-ticker-ksei.json.golden diff --git a/tests/golden/ownership-ticker-ksei.table.golden b/crates/idx-cli/tests/golden/ownership-ticker-ksei.table.golden similarity index 100% rename from tests/golden/ownership-ticker-ksei.table.golden rename to crates/idx-cli/tests/golden/ownership-ticker-ksei.table.golden diff --git a/tests/golden/ownership-ticker.json.golden b/crates/idx-cli/tests/golden/ownership-ticker.json.golden similarity index 100% rename from tests/golden/ownership-ticker.json.golden rename to crates/idx-cli/tests/golden/ownership-ticker.json.golden diff --git a/tests/golden/ownership-ticker.table.golden b/crates/idx-cli/tests/golden/ownership-ticker.table.golden similarity index 100% rename from tests/golden/ownership-ticker.table.golden rename to crates/idx-cli/tests/golden/ownership-ticker.table.golden diff --git a/tests/golden/stocks-compare-stdin.json.golden b/crates/idx-cli/tests/golden/stocks-compare-stdin.json.golden similarity index 100% rename from tests/golden/stocks-compare-stdin.json.golden rename to crates/idx-cli/tests/golden/stocks-compare-stdin.json.golden diff --git a/tests/golden/stocks-compare-stdin.table.golden b/crates/idx-cli/tests/golden/stocks-compare-stdin.table.golden similarity index 100% rename from tests/golden/stocks-compare-stdin.table.golden rename to crates/idx-cli/tests/golden/stocks-compare-stdin.table.golden diff --git a/tests/golden/stocks-compare.json.golden b/crates/idx-cli/tests/golden/stocks-compare.json.golden similarity index 100% rename from tests/golden/stocks-compare.json.golden rename to crates/idx-cli/tests/golden/stocks-compare.json.golden diff --git a/tests/golden/stocks-compare.table.golden b/crates/idx-cli/tests/golden/stocks-compare.table.golden similarity index 100% rename from tests/golden/stocks-compare.table.golden rename to crates/idx-cli/tests/golden/stocks-compare.table.golden diff --git a/tests/golden/stocks-earnings-forecast-annual.json.golden b/crates/idx-cli/tests/golden/stocks-earnings-forecast-annual.json.golden similarity index 100% rename from tests/golden/stocks-earnings-forecast-annual.json.golden rename to crates/idx-cli/tests/golden/stocks-earnings-forecast-annual.json.golden diff --git a/tests/golden/stocks-earnings-forecast-annual.table.golden b/crates/idx-cli/tests/golden/stocks-earnings-forecast-annual.table.golden similarity index 100% rename from tests/golden/stocks-earnings-forecast-annual.table.golden rename to crates/idx-cli/tests/golden/stocks-earnings-forecast-annual.table.golden diff --git a/tests/golden/stocks-earnings-history-quarterly.json.golden b/crates/idx-cli/tests/golden/stocks-earnings-history-quarterly.json.golden similarity index 100% rename from tests/golden/stocks-earnings-history-quarterly.json.golden rename to crates/idx-cli/tests/golden/stocks-earnings-history-quarterly.json.golden diff --git a/tests/golden/stocks-earnings-history-quarterly.table.golden b/crates/idx-cli/tests/golden/stocks-earnings-history-quarterly.table.golden similarity index 100% rename from tests/golden/stocks-earnings-history-quarterly.table.golden rename to crates/idx-cli/tests/golden/stocks-earnings-history-quarterly.table.golden diff --git a/tests/golden/stocks-earnings.json.golden b/crates/idx-cli/tests/golden/stocks-earnings.json.golden similarity index 100% rename from tests/golden/stocks-earnings.json.golden rename to crates/idx-cli/tests/golden/stocks-earnings.json.golden diff --git a/tests/golden/stocks-earnings.table.golden b/crates/idx-cli/tests/golden/stocks-earnings.table.golden similarity index 100% rename from tests/golden/stocks-earnings.table.golden rename to crates/idx-cli/tests/golden/stocks-earnings.table.golden diff --git a/tests/golden/stocks-financials-income.json.golden b/crates/idx-cli/tests/golden/stocks-financials-income.json.golden similarity index 100% rename from tests/golden/stocks-financials-income.json.golden rename to crates/idx-cli/tests/golden/stocks-financials-income.json.golden diff --git a/tests/golden/stocks-financials-income.table.golden b/crates/idx-cli/tests/golden/stocks-financials-income.table.golden similarity index 100% rename from tests/golden/stocks-financials-income.table.golden rename to crates/idx-cli/tests/golden/stocks-financials-income.table.golden diff --git a/tests/golden/stocks-financials.json.golden b/crates/idx-cli/tests/golden/stocks-financials.json.golden similarity index 100% rename from tests/golden/stocks-financials.json.golden rename to crates/idx-cli/tests/golden/stocks-financials.json.golden diff --git a/tests/golden/stocks-financials.table.golden b/crates/idx-cli/tests/golden/stocks-financials.table.golden similarity index 100% rename from tests/golden/stocks-financials.table.golden rename to crates/idx-cli/tests/golden/stocks-financials.table.golden diff --git a/tests/golden/stocks-fundamental.json.golden b/crates/idx-cli/tests/golden/stocks-fundamental.json.golden similarity index 100% rename from tests/golden/stocks-fundamental.json.golden rename to crates/idx-cli/tests/golden/stocks-fundamental.json.golden diff --git a/tests/golden/stocks-fundamental.jsonl.golden b/crates/idx-cli/tests/golden/stocks-fundamental.jsonl.golden similarity index 100% rename from tests/golden/stocks-fundamental.jsonl.golden rename to crates/idx-cli/tests/golden/stocks-fundamental.jsonl.golden diff --git a/tests/golden/stocks-fundamental.table.golden b/crates/idx-cli/tests/golden/stocks-fundamental.table.golden similarity index 100% rename from tests/golden/stocks-fundamental.table.golden rename to crates/idx-cli/tests/golden/stocks-fundamental.table.golden diff --git a/tests/golden/stocks-growth.json.golden b/crates/idx-cli/tests/golden/stocks-growth.json.golden similarity index 100% rename from tests/golden/stocks-growth.json.golden rename to crates/idx-cli/tests/golden/stocks-growth.json.golden diff --git a/tests/golden/stocks-growth.table.golden b/crates/idx-cli/tests/golden/stocks-growth.table.golden similarity index 100% rename from tests/golden/stocks-growth.table.golden rename to crates/idx-cli/tests/golden/stocks-growth.table.golden diff --git a/tests/golden/stocks-history-msn.json.golden b/crates/idx-cli/tests/golden/stocks-history-msn.json.golden similarity index 100% rename from tests/golden/stocks-history-msn.json.golden rename to crates/idx-cli/tests/golden/stocks-history-msn.json.golden diff --git a/tests/golden/stocks-history-msn.table.golden b/crates/idx-cli/tests/golden/stocks-history-msn.table.golden similarity index 100% rename from tests/golden/stocks-history-msn.table.golden rename to crates/idx-cli/tests/golden/stocks-history-msn.table.golden diff --git a/tests/golden/stocks-history.json.golden b/crates/idx-cli/tests/golden/stocks-history.json.golden similarity index 100% rename from tests/golden/stocks-history.json.golden rename to crates/idx-cli/tests/golden/stocks-history.json.golden diff --git a/tests/golden/stocks-history.jsonl.golden b/crates/idx-cli/tests/golden/stocks-history.jsonl.golden similarity index 100% rename from tests/golden/stocks-history.jsonl.golden rename to crates/idx-cli/tests/golden/stocks-history.jsonl.golden diff --git a/tests/golden/stocks-history.table.golden b/crates/idx-cli/tests/golden/stocks-history.table.golden similarity index 100% rename from tests/golden/stocks-history.table.golden rename to crates/idx-cli/tests/golden/stocks-history.table.golden diff --git a/tests/golden/stocks-insights.json.golden b/crates/idx-cli/tests/golden/stocks-insights.json.golden similarity index 100% rename from tests/golden/stocks-insights.json.golden rename to crates/idx-cli/tests/golden/stocks-insights.json.golden diff --git a/tests/golden/stocks-insights.table.golden b/crates/idx-cli/tests/golden/stocks-insights.table.golden similarity index 100% rename from tests/golden/stocks-insights.table.golden rename to crates/idx-cli/tests/golden/stocks-insights.table.golden diff --git a/tests/golden/stocks-news.json.golden b/crates/idx-cli/tests/golden/stocks-news.json.golden similarity index 100% rename from tests/golden/stocks-news.json.golden rename to crates/idx-cli/tests/golden/stocks-news.json.golden diff --git a/tests/golden/stocks-news.table.golden b/crates/idx-cli/tests/golden/stocks-news.table.golden similarity index 100% rename from tests/golden/stocks-news.table.golden rename to crates/idx-cli/tests/golden/stocks-news.table.golden diff --git a/tests/golden/stocks-profile.json.golden b/crates/idx-cli/tests/golden/stocks-profile.json.golden similarity index 100% rename from tests/golden/stocks-profile.json.golden rename to crates/idx-cli/tests/golden/stocks-profile.json.golden diff --git a/tests/golden/stocks-profile.table.golden b/crates/idx-cli/tests/golden/stocks-profile.table.golden similarity index 100% rename from tests/golden/stocks-profile.table.golden rename to crates/idx-cli/tests/golden/stocks-profile.table.golden diff --git a/tests/golden/stocks-quote-multi.json.golden b/crates/idx-cli/tests/golden/stocks-quote-multi.json.golden similarity index 100% rename from tests/golden/stocks-quote-multi.json.golden rename to crates/idx-cli/tests/golden/stocks-quote-multi.json.golden diff --git a/tests/golden/stocks-quote-multi.jsonl.golden b/crates/idx-cli/tests/golden/stocks-quote-multi.jsonl.golden similarity index 100% rename from tests/golden/stocks-quote-multi.jsonl.golden rename to crates/idx-cli/tests/golden/stocks-quote-multi.jsonl.golden diff --git a/tests/golden/stocks-quote-multi.table.golden b/crates/idx-cli/tests/golden/stocks-quote-multi.table.golden similarity index 100% rename from tests/golden/stocks-quote-multi.table.golden rename to crates/idx-cli/tests/golden/stocks-quote-multi.table.golden diff --git a/tests/golden/stocks-quote-offline-warm.json.golden b/crates/idx-cli/tests/golden/stocks-quote-offline-warm.json.golden similarity index 100% rename from tests/golden/stocks-quote-offline-warm.json.golden rename to crates/idx-cli/tests/golden/stocks-quote-offline-warm.json.golden diff --git a/tests/golden/stocks-quote-offline-warm.table.golden b/crates/idx-cli/tests/golden/stocks-quote-offline-warm.table.golden similarity index 100% rename from tests/golden/stocks-quote-offline-warm.table.golden rename to crates/idx-cli/tests/golden/stocks-quote-offline-warm.table.golden diff --git a/tests/golden/stocks-quote-stdin.json.golden b/crates/idx-cli/tests/golden/stocks-quote-stdin.json.golden similarity index 100% rename from tests/golden/stocks-quote-stdin.json.golden rename to crates/idx-cli/tests/golden/stocks-quote-stdin.json.golden diff --git a/tests/golden/stocks-quote-stdin.table.golden b/crates/idx-cli/tests/golden/stocks-quote-stdin.table.golden similarity index 100% rename from tests/golden/stocks-quote-stdin.table.golden rename to crates/idx-cli/tests/golden/stocks-quote-stdin.table.golden diff --git a/tests/golden/stocks-quote.json.golden b/crates/idx-cli/tests/golden/stocks-quote.json.golden similarity index 100% rename from tests/golden/stocks-quote.json.golden rename to crates/idx-cli/tests/golden/stocks-quote.json.golden diff --git a/tests/golden/stocks-quote.table.golden b/crates/idx-cli/tests/golden/stocks-quote.table.golden similarity index 100% rename from tests/golden/stocks-quote.table.golden rename to crates/idx-cli/tests/golden/stocks-quote.table.golden diff --git a/tests/golden/stocks-risk.json.golden b/crates/idx-cli/tests/golden/stocks-risk.json.golden similarity index 100% rename from tests/golden/stocks-risk.json.golden rename to crates/idx-cli/tests/golden/stocks-risk.json.golden diff --git a/tests/golden/stocks-risk.table.golden b/crates/idx-cli/tests/golden/stocks-risk.table.golden similarity index 100% rename from tests/golden/stocks-risk.table.golden rename to crates/idx-cli/tests/golden/stocks-risk.table.golden diff --git a/tests/golden/stocks-screen-high-volume.json.golden b/crates/idx-cli/tests/golden/stocks-screen-high-volume.json.golden similarity index 100% rename from tests/golden/stocks-screen-high-volume.json.golden rename to crates/idx-cli/tests/golden/stocks-screen-high-volume.json.golden diff --git a/tests/golden/stocks-screen-high-volume.table.golden b/crates/idx-cli/tests/golden/stocks-screen-high-volume.table.golden similarity index 100% rename from tests/golden/stocks-screen-high-volume.table.golden rename to crates/idx-cli/tests/golden/stocks-screen-high-volume.table.golden diff --git a/tests/golden/stocks-screen-large-cap.json.golden b/crates/idx-cli/tests/golden/stocks-screen-large-cap.json.golden similarity index 100% rename from tests/golden/stocks-screen-large-cap.json.golden rename to crates/idx-cli/tests/golden/stocks-screen-large-cap.json.golden diff --git a/tests/golden/stocks-screen-large-cap.table.golden b/crates/idx-cli/tests/golden/stocks-screen-large-cap.table.golden similarity index 100% rename from tests/golden/stocks-screen-large-cap.table.golden rename to crates/idx-cli/tests/golden/stocks-screen-large-cap.table.golden diff --git a/tests/golden/stocks-screen-worst.json.golden b/crates/idx-cli/tests/golden/stocks-screen-worst.json.golden similarity index 100% rename from tests/golden/stocks-screen-worst.json.golden rename to crates/idx-cli/tests/golden/stocks-screen-worst.json.golden diff --git a/tests/golden/stocks-screen-worst.table.golden b/crates/idx-cli/tests/golden/stocks-screen-worst.table.golden similarity index 100% rename from tests/golden/stocks-screen-worst.table.golden rename to crates/idx-cli/tests/golden/stocks-screen-worst.table.golden diff --git a/tests/golden/stocks-screen.json.golden b/crates/idx-cli/tests/golden/stocks-screen.json.golden similarity index 100% rename from tests/golden/stocks-screen.json.golden rename to crates/idx-cli/tests/golden/stocks-screen.json.golden diff --git a/tests/golden/stocks-screen.table.golden b/crates/idx-cli/tests/golden/stocks-screen.table.golden similarity index 100% rename from tests/golden/stocks-screen.table.golden rename to crates/idx-cli/tests/golden/stocks-screen.table.golden diff --git a/tests/golden/stocks-sentiment.json.golden b/crates/idx-cli/tests/golden/stocks-sentiment.json.golden similarity index 100% rename from tests/golden/stocks-sentiment.json.golden rename to crates/idx-cli/tests/golden/stocks-sentiment.json.golden diff --git a/tests/golden/stocks-sentiment.table.golden b/crates/idx-cli/tests/golden/stocks-sentiment.table.golden similarity index 100% rename from tests/golden/stocks-sentiment.table.golden rename to crates/idx-cli/tests/golden/stocks-sentiment.table.golden diff --git a/tests/golden/stocks-technical.json.golden b/crates/idx-cli/tests/golden/stocks-technical.json.golden similarity index 100% rename from tests/golden/stocks-technical.json.golden rename to crates/idx-cli/tests/golden/stocks-technical.json.golden diff --git a/tests/golden/stocks-technical.table.golden b/crates/idx-cli/tests/golden/stocks-technical.table.golden similarity index 100% rename from tests/golden/stocks-technical.table.golden rename to crates/idx-cli/tests/golden/stocks-technical.table.golden diff --git a/tests/golden/stocks-valuation-nonfinite.json.golden b/crates/idx-cli/tests/golden/stocks-valuation-nonfinite.json.golden similarity index 100% rename from tests/golden/stocks-valuation-nonfinite.json.golden rename to crates/idx-cli/tests/golden/stocks-valuation-nonfinite.json.golden diff --git a/tests/golden/stocks-valuation-nonfinite.table.golden b/crates/idx-cli/tests/golden/stocks-valuation-nonfinite.table.golden similarity index 100% rename from tests/golden/stocks-valuation-nonfinite.table.golden rename to crates/idx-cli/tests/golden/stocks-valuation-nonfinite.table.golden diff --git a/tests/golden/stocks-valuation.json.golden b/crates/idx-cli/tests/golden/stocks-valuation.json.golden similarity index 100% rename from tests/golden/stocks-valuation.json.golden rename to crates/idx-cli/tests/golden/stocks-valuation.json.golden diff --git a/tests/golden/stocks-valuation.table.golden b/crates/idx-cli/tests/golden/stocks-valuation.table.golden similarity index 100% rename from tests/golden/stocks-valuation.table.golden rename to crates/idx-cli/tests/golden/stocks-valuation.table.golden diff --git a/tests/golden/yahoo-fundamental.json.golden b/crates/idx-cli/tests/golden/yahoo-fundamental.json.golden similarity index 100% rename from tests/golden/yahoo-fundamental.json.golden rename to crates/idx-cli/tests/golden/yahoo-fundamental.json.golden diff --git a/tests/golden/yahoo-fundamental.table.golden b/crates/idx-cli/tests/golden/yahoo-fundamental.table.golden similarity index 100% rename from tests/golden/yahoo-fundamental.table.golden rename to crates/idx-cli/tests/golden/yahoo-fundamental.table.golden diff --git a/tests/golden/yahoo-history.json.golden b/crates/idx-cli/tests/golden/yahoo-history.json.golden similarity index 100% rename from tests/golden/yahoo-history.json.golden rename to crates/idx-cli/tests/golden/yahoo-history.json.golden diff --git a/tests/golden/yahoo-history.table.golden b/crates/idx-cli/tests/golden/yahoo-history.table.golden similarity index 100% rename from tests/golden/yahoo-history.table.golden rename to crates/idx-cli/tests/golden/yahoo-history.table.golden diff --git a/tests/golden/yahoo-quote.json.golden b/crates/idx-cli/tests/golden/yahoo-quote.json.golden similarity index 100% rename from tests/golden/yahoo-quote.json.golden rename to crates/idx-cli/tests/golden/yahoo-quote.json.golden diff --git a/tests/golden/yahoo-quote.table.golden b/crates/idx-cli/tests/golden/yahoo-quote.table.golden similarity index 100% rename from tests/golden/yahoo-quote.table.golden rename to crates/idx-cli/tests/golden/yahoo-quote.table.golden diff --git a/crates/idx-core/Cargo.toml b/crates/idx-core/Cargo.toml new file mode 100644 index 0000000..70b9b25 --- /dev/null +++ b/crates/idx-core/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "idx-core" +description = "Data layer for Indonesian stock market (IDX) market, financial, and ownership data" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +readme = "../../README.md" +keywords = ["idx", "stocks", "indonesia", "finance"] +categories = ["finance"] +include = ["/Cargo.toml", "/LICENSE", "/src/**", "/tests/fixtures/**"] + +[features] +default = ["ownership"] +ownership = ["dep:rusqlite", "dep:quick-xml", "dep:sha2", "dep:zip"] +# Derives clap::ValueEnum on enums frontends expose as CLI arguments. +clap = ["dep:clap"] + +[dependencies] +clap = { workspace = true, optional = true } +ureq.workspace = true +serde.workspace = true +serde_json.workspace = true +toml.workspace = true +directories = "5" +chrono.workspace = true +thiserror = "2" +fastrand = "2" +rusqlite = { workspace = true, optional = true } +quick-xml = { version = "0.37", optional = true } +sha2 = { workspace = true, optional = true } +zip = { workspace = true, optional = true } diff --git a/crates/idx-core/LICENSE b/crates/idx-core/LICENSE new file mode 120000 index 0000000..30cff74 --- /dev/null +++ b/crates/idx-core/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/src/analysis/fundamental.rs b/crates/idx-core/src/analysis/fundamental.rs similarity index 100% rename from src/analysis/fundamental.rs rename to crates/idx-core/src/analysis/fundamental.rs diff --git a/src/analysis/mod.rs b/crates/idx-core/src/analysis/mod.rs similarity index 100% rename from src/analysis/mod.rs rename to crates/idx-core/src/analysis/mod.rs diff --git a/src/analysis/signals.rs b/crates/idx-core/src/analysis/signals.rs similarity index 100% rename from src/analysis/signals.rs rename to crates/idx-core/src/analysis/signals.rs diff --git a/src/analysis/technical.rs b/crates/idx-core/src/analysis/technical.rs similarity index 100% rename from src/analysis/technical.rs rename to crates/idx-core/src/analysis/technical.rs diff --git a/src/api/mod.rs b/crates/idx-core/src/api/mod.rs similarity index 100% rename from src/api/mod.rs rename to crates/idx-core/src/api/mod.rs diff --git a/src/api/msn/bing.rs b/crates/idx-core/src/api/msn/bing.rs similarity index 100% rename from src/api/msn/bing.rs rename to crates/idx-core/src/api/msn/bing.rs diff --git a/src/api/msn/client.rs b/crates/idx-core/src/api/msn/client.rs similarity index 100% rename from src/api/msn/client.rs rename to crates/idx-core/src/api/msn/client.rs diff --git a/src/api/msn/map.rs b/crates/idx-core/src/api/msn/map.rs similarity index 100% rename from src/api/msn/map.rs rename to crates/idx-core/src/api/msn/map.rs diff --git a/src/api/msn/mod.rs b/crates/idx-core/src/api/msn/mod.rs similarity index 100% rename from src/api/msn/mod.rs rename to crates/idx-core/src/api/msn/mod.rs diff --git a/src/api/msn/parse.rs b/crates/idx-core/src/api/msn/parse.rs similarity index 100% rename from src/api/msn/parse.rs rename to crates/idx-core/src/api/msn/parse.rs diff --git a/src/api/msn/raw_types.rs b/crates/idx-core/src/api/msn/raw_types.rs similarity index 100% rename from src/api/msn/raw_types.rs rename to crates/idx-core/src/api/msn/raw_types.rs diff --git a/src/api/msn/symbol_ids.tsv b/crates/idx-core/src/api/msn/symbol_ids.tsv similarity index 100% rename from src/api/msn/symbol_ids.tsv rename to crates/idx-core/src/api/msn/symbol_ids.tsv diff --git a/src/api/msn/symbols.rs b/crates/idx-core/src/api/msn/symbols.rs similarity index 100% rename from src/api/msn/symbols.rs rename to crates/idx-core/src/api/msn/symbols.rs diff --git a/src/api/types.rs b/crates/idx-core/src/api/types.rs similarity index 92% rename from src/api/types.rs rename to crates/idx-core/src/api/types.rs index d68d52b..130046d 100644 --- a/src/api/types.rs +++ b/crates/idx-core/src/api/types.rs @@ -1,5 +1,4 @@ use chrono::{DateTime, FixedOffset, NaiveDate}; -use clap::ValueEnum; use serde::{Deserialize, Deserializer, Serialize, de::Error as _}; /// Snapshot quote data normalized from Yahoo Finance `/v8/finance/chart` response. @@ -273,23 +272,24 @@ where }) } -#[derive(Debug, Clone, Serialize, Deserialize, ValueEnum)] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] pub enum Period { - #[value(name = "1d")] + #[cfg_attr(feature = "clap", value(name = "1d"))] OneDay, - #[value(name = "5d")] + #[cfg_attr(feature = "clap", value(name = "5d"))] FiveDays, - #[value(name = "1mo")] + #[cfg_attr(feature = "clap", value(name = "1mo"))] OneMonth, - #[value(name = "3mo")] + #[cfg_attr(feature = "clap", value(name = "3mo"))] ThreeMonths, - #[value(name = "6mo")] + #[cfg_attr(feature = "clap", value(name = "6mo"))] SixMonths, - #[value(name = "1y")] + #[cfg_attr(feature = "clap", value(name = "1y"))] OneYear, - #[value(name = "2y")] + #[cfg_attr(feature = "clap", value(name = "2y"))] TwoYears, - #[value(name = "5y")] + #[cfg_attr(feature = "clap", value(name = "5y"))] FiveYears, } @@ -308,13 +308,14 @@ impl Period { } } -#[derive(Debug, Clone, Serialize, Deserialize, ValueEnum)] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] pub enum Interval { - #[value(name = "1d")] + #[cfg_attr(feature = "clap", value(name = "1d"))] Day, - #[value(name = "1wk")] + #[cfg_attr(feature = "clap", value(name = "1wk"))] Week, - #[value(name = "1mo")] + #[cfg_attr(feature = "clap", value(name = "1mo"))] Month, } diff --git a/src/api/yahoo/client.rs b/crates/idx-core/src/api/yahoo/client.rs similarity index 100% rename from src/api/yahoo/client.rs rename to crates/idx-core/src/api/yahoo/client.rs diff --git a/src/api/yahoo/map.rs b/crates/idx-core/src/api/yahoo/map.rs similarity index 100% rename from src/api/yahoo/map.rs rename to crates/idx-core/src/api/yahoo/map.rs diff --git a/src/api/yahoo/mod.rs b/crates/idx-core/src/api/yahoo/mod.rs similarity index 100% rename from src/api/yahoo/mod.rs rename to crates/idx-core/src/api/yahoo/mod.rs diff --git a/src/api/yahoo/parse.rs b/crates/idx-core/src/api/yahoo/parse.rs similarity index 100% rename from src/api/yahoo/parse.rs rename to crates/idx-core/src/api/yahoo/parse.rs diff --git a/src/api/yahoo/raw_types.rs b/crates/idx-core/src/api/yahoo/raw_types.rs similarity index 100% rename from src/api/yahoo/raw_types.rs rename to crates/idx-core/src/api/yahoo/raw_types.rs diff --git a/src/cache.rs b/crates/idx-core/src/cache.rs similarity index 100% rename from src/cache.rs rename to crates/idx-core/src/cache.rs diff --git a/src/config.rs b/crates/idx-core/src/config.rs similarity index 75% rename from src/config.rs rename to crates/idx-core/src/config.rs index b7b3ba9..6852533 100644 --- a/src/config.rs +++ b/crates/idx-core/src/config.rs @@ -1,14 +1,16 @@ +//! Config file location, provider selection, and `config.toml` read/write. +//! Presentation settings (output format, color) are resolved by frontends. + use std::fs; -use std::io::IsTerminal; use std::path::PathBuf; -use clap::ValueEnum; use directories::ProjectDirs; use serde::Deserialize; -use crate::cli::Cli; use crate::error::IdxError; -use crate::output::{OutputFormat, OutputSetting}; + +/// Values accepted for `general.output`; frontends decide what they mean. +pub const OUTPUT_VALUES: &[&str] = &["auto", "table", "json", "jsonl"]; #[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] @@ -17,9 +19,10 @@ pub enum ProviderKind { Msn, } -#[derive(Debug, Clone, Copy, Deserialize, ValueEnum, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] #[serde(rename_all = "lowercase")] -#[value(rename_all = "lower")] +#[cfg_attr(feature = "clap", value(rename_all = "lower"))] pub enum HistoryProviderKind { Auto, Yahoo, @@ -34,7 +37,7 @@ impl ProviderKind { } } - fn parse(value: &str) -> Result { + pub fn parse(value: &str) -> Result { if value.eq_ignore_ascii_case("yahoo") { Ok(Self::Yahoo) } else if value.eq_ignore_ascii_case("msn") { @@ -56,7 +59,7 @@ impl HistoryProviderKind { } } - fn parse(value: &str) -> Result { + pub fn parse(value: &str) -> Result { if value.eq_ignore_ascii_case("auto") { Ok(Self::Auto) } else if value.eq_ignore_ascii_case("yahoo") { @@ -71,129 +74,7 @@ impl HistoryProviderKind { } } -#[derive(Debug, Clone)] -pub struct IdxConfig { - pub provider: ProviderKind, - pub history_provider: HistoryProviderKind, - pub exchange: String, - pub output: OutputFormat, - pub no_color: bool, - pub quote_ttl: u64, - pub fundamental_ttl: u64, -} - -#[derive(Debug, Deserialize, Default)] -struct FileConfig { - general: Option, - cache: Option, -} - -#[derive(Debug, Deserialize, Default)] -struct FileGeneral { - provider: Option, - history_provider: Option, - exchange: Option, - output: Option, - color: Option, -} - -#[derive(Debug, Deserialize, Default)] -struct FileCache { - quote_ttl: Option, - fundamental_ttl: Option, -} - -impl Default for IdxConfig { - fn default() -> Self { - Self { - provider: ProviderKind::Msn, - history_provider: HistoryProviderKind::Auto, - exchange: "JK".to_string(), - output: OutputSetting::Auto.resolve(stdout_is_terminal()), - no_color: false, - quote_ttl: 300, - fundamental_ttl: 3600, - } - } -} - -impl IdxConfig { - pub fn load_with_cli(cli: &Cli) -> Result { - let mut cfg = Self::load()?; - - if let Ok(provider) = std::env::var("IDX_PROVIDER") { - cfg.provider = ProviderKind::parse(&provider)?; - } - if let Ok(history_provider) = std::env::var("IDX_HISTORY_PROVIDER") { - cfg.history_provider = HistoryProviderKind::parse(&history_provider)?; - } - if let Ok(exchange) = std::env::var("IDX_EXCHANGE") { - cfg.exchange = exchange; - } - if let Ok(output) = std::env::var("IDX_OUTPUT") { - let setting = OutputSetting::parse(&output).ok_or_else(|| { - IdxError::ConfigError(format!( - "invalid IDX_OUTPUT value: '{output}', expected 'auto', 'table', 'json', or 'jsonl'" - )) - })?; - cfg.output = setting.resolve(stdout_is_terminal()); - } - if let Ok(no_color) = std::env::var("IDX_NO_COLOR") { - cfg.no_color = no_color == "1" || no_color.eq_ignore_ascii_case("true"); - } - if let Some(parsed) = parse_env_ttl("IDX_CACHE_QUOTE_TTL")? { - cfg.quote_ttl = parsed; - } - if let Some(parsed) = parse_env_ttl("IDX_CACHE_FUNDAMENTAL_TTL")? { - cfg.fundamental_ttl = parsed; - } - - if let Some(output) = cli.output { - cfg.output = output.resolve(stdout_is_terminal()); - } - cfg.no_color = cfg.no_color || cli.no_color || !stdout_is_terminal(); - - Ok(cfg) - } - - pub fn load() -> Result { - let mut cfg = Self::default(); - let path = config_path()?; - if path.exists() { - let raw = fs::read_to_string(&path).map_err(|e| IdxError::Io(e.to_string()))?; - let parsed: FileConfig = - toml::from_str(&raw).map_err(|e| IdxError::ConfigError(e.to_string()))?; - if let Some(general) = parsed.general { - if let Some(provider) = general.provider { - cfg.provider = provider; - } - if let Some(history_provider) = general.history_provider { - cfg.history_provider = history_provider; - } - if let Some(exchange) = general.exchange { - cfg.exchange = exchange; - } - if let Some(output) = general.output { - cfg.output = output.resolve(stdout_is_terminal()); - } - if let Some(color) = general.color { - cfg.no_color = !color; - } - } - if let Some(cache) = parsed.cache { - if let Some(v) = cache.quote_ttl { - cfg.quote_ttl = v; - } - if let Some(v) = cache.fundamental_ttl { - cfg.fundamental_ttl = v; - } - } - } - Ok(cfg) - } -} - -fn parse_env_ttl(name: &str) -> Result, IdxError> { +pub fn parse_env_ttl(name: &str) -> Result, IdxError> { let Ok(raw) = std::env::var(name) else { return Ok(None); }; @@ -214,10 +95,6 @@ fn parse_env_ttl(name: &str) -> Result, IdxError> { Ok(Some(parsed)) } -pub fn stdout_is_terminal() -> bool { - std::io::stdout().is_terminal() -} - pub fn default_config_toml() -> String { "[general]\nprovider = \"msn\"\nhistory_provider = \"auto\"\nexchange = \"JK\"\noutput = \"auto\"\ncolor = true\n\n[cache]\nquote_ttl = 300\nfundamental_ttl = 3600\n".to_string() } @@ -294,7 +171,7 @@ fn normalize_config_value(key: &str, value: &str) -> Result { - if OutputSetting::parse(value).is_none() { + if !OUTPUT_VALUES.iter().any(|v| v.eq_ignore_ascii_case(value)) { return Err(IdxError::InvalidInput(format!( "invalid output format '{value}': expected 'auto', 'table', 'json', or 'jsonl'" ))); @@ -381,17 +258,6 @@ pub fn set_config_value(key: &str, value: &str) -> Result<(), IdxError> { #[cfg(test)] mod tests { - use super::IdxConfig; - - #[test] - fn default_values_are_sane() { - let cfg = IdxConfig::default(); - assert_eq!(cfg.provider, super::ProviderKind::Msn); - assert_eq!(cfg.history_provider, super::HistoryProviderKind::Auto); - assert_eq!(cfg.exchange, "JK"); - assert_eq!(cfg.quote_ttl, 300); - } - #[test] fn parses_provider_values() { assert_eq!( diff --git a/src/curl_impersonate.rs b/crates/idx-core/src/curl_impersonate.rs similarity index 100% rename from src/curl_impersonate.rs rename to crates/idx-core/src/curl_impersonate.rs diff --git a/src/error.rs b/crates/idx-core/src/error.rs similarity index 100% rename from src/error.rs rename to crates/idx-core/src/error.rs diff --git a/crates/idx-core/src/lib.rs b/crates/idx-core/src/lib.rs new file mode 100644 index 0000000..39170cd --- /dev/null +++ b/crates/idx-core/src/lib.rs @@ -0,0 +1,39 @@ +//! idx-core: retrieval and analysis of Indonesian (IDX) market, financial, and +//! investor data, shared by the `idx` CLI and other frontends. +//! See docs/DESIGN.md. + +pub mod analysis; +pub mod api; +pub mod cache; +pub mod config; +pub mod curl_impersonate; +pub mod error; +#[cfg(feature = "ownership")] +pub mod ownership; + +pub mod runtime { + use std::fmt::Display; + use std::sync::atomic::{AtomicBool, Ordering}; + + static QUIET: AtomicBool = AtomicBool::new(false); + + pub fn set_quiet(quiet: bool) { + QUIET.store(quiet, Ordering::Relaxed); + } + + pub fn is_quiet() -> bool { + QUIET.load(Ordering::Relaxed) + } + + pub fn info(message: impl Display) { + if !is_quiet() { + eprintln!("info: {message}"); + } + } + + pub fn warn(message: impl Display) { + if !is_quiet() { + eprintln!("warning: {message}"); + } + } +} diff --git a/src/ownership/archive.rs b/crates/idx-core/src/ownership/archive.rs similarity index 100% rename from src/ownership/archive.rs rename to crates/idx-core/src/ownership/archive.rs diff --git a/src/ownership/db.rs b/crates/idx-core/src/ownership/db.rs similarity index 99% rename from src/ownership/db.rs rename to crates/idx-core/src/ownership/db.rs index af4b936..d98dbce 100644 --- a/src/ownership/db.rs +++ b/crates/idx-core/src/ownership/db.rs @@ -5,7 +5,7 @@ use chrono::NaiveDate; use directories::ProjectDirs; use rusqlite::{Connection, params}; -use crate::config::{IdxConfig, get_config_value}; +use crate::config::get_config_value; use crate::error::IdxError; use crate::ownership::search; use crate::ownership::types::{ @@ -128,8 +128,8 @@ pub fn ensure_schema(conn: &Connection) -> Result<(), IdxError> { } /// Open ownership database connection and run idempotent schema migration. -pub fn open_db(config: &IdxConfig) -> Result { - let db_path = db_path(config)?; +pub fn open_db() -> Result { + let db_path = db_path()?; if let Some(parent) = db_path.parent() { fs::create_dir_all(parent).map_err(|e| IdxError::DatabaseError(e.to_string()))?; @@ -1204,7 +1204,7 @@ pub fn compute_concentration(percentages_bps: &[i64]) -> ConcentrationMetrics { } } -pub fn db_path(_config: &IdxConfig) -> Result { +pub fn db_path() -> Result { if let Some(custom_path) = get_config_value("ownership.db_path")? { let trimmed = custom_path.trim(); if !trimmed.is_empty() { diff --git a/src/ownership/entities.rs b/crates/idx-core/src/ownership/entities.rs similarity index 100% rename from src/ownership/entities.rs rename to crates/idx-core/src/ownership/entities.rs diff --git a/src/ownership/graph.rs b/crates/idx-core/src/ownership/graph.rs similarity index 100% rename from src/ownership/graph.rs rename to crates/idx-core/src/ownership/graph.rs diff --git a/src/ownership/mod.rs b/crates/idx-core/src/ownership/mod.rs similarity index 100% rename from src/ownership/mod.rs rename to crates/idx-core/src/ownership/mod.rs diff --git a/src/ownership/parser.rs b/crates/idx-core/src/ownership/parser.rs similarity index 100% rename from src/ownership/parser.rs rename to crates/idx-core/src/ownership/parser.rs diff --git a/src/ownership/remote.rs b/crates/idx-core/src/ownership/remote.rs similarity index 100% rename from src/ownership/remote.rs rename to crates/idx-core/src/ownership/remote.rs diff --git a/src/ownership/search.rs b/crates/idx-core/src/ownership/search.rs similarity index 100% rename from src/ownership/search.rs rename to crates/idx-core/src/ownership/search.rs diff --git a/src/ownership/snapshot.rs b/crates/idx-core/src/ownership/snapshot.rs similarity index 100% rename from src/ownership/snapshot.rs rename to crates/idx-core/src/ownership/snapshot.rs diff --git a/src/ownership/types.rs b/crates/idx-core/src/ownership/types.rs similarity index 100% rename from src/ownership/types.rs rename to crates/idx-core/src/ownership/types.rs diff --git a/src/ownership/xlsx.rs b/crates/idx-core/src/ownership/xlsx.rs similarity index 100% rename from src/ownership/xlsx.rs rename to crates/idx-core/src/ownership/xlsx.rs diff --git a/tests/fixtures/bing_buyers_bbca.json b/crates/idx-core/tests/fixtures/bing_buyers_bbca.json similarity index 100% rename from tests/fixtures/bing_buyers_bbca.json rename to crates/idx-core/tests/fixtures/bing_buyers_bbca.json diff --git a/tests/fixtures/bing_holders_bbca.json b/crates/idx-core/tests/fixtures/bing_holders_bbca.json similarity index 100% rename from tests/fixtures/bing_holders_bbca.json rename to crates/idx-core/tests/fixtures/bing_holders_bbca.json diff --git a/tests/fixtures/chart_bbca_1d.json b/crates/idx-core/tests/fixtures/chart_bbca_1d.json similarity index 100% rename from tests/fixtures/chart_bbca_1d.json rename to crates/idx-core/tests/fixtures/chart_bbca_1d.json diff --git a/tests/fixtures/chart_bbca_3mo.json b/crates/idx-core/tests/fixtures/chart_bbca_3mo.json similarity index 100% rename from tests/fixtures/chart_bbca_3mo.json rename to crates/idx-core/tests/fixtures/chart_bbca_3mo.json diff --git a/tests/fixtures/chart_bbca_with_gap.json b/crates/idx-core/tests/fixtures/chart_bbca_with_gap.json similarity index 100% rename from tests/fixtures/chart_bbca_with_gap.json rename to crates/idx-core/tests/fixtures/chart_bbca_with_gap.json diff --git a/tests/fixtures/idx_announcement_kepemilikan.json b/crates/idx-core/tests/fixtures/idx_announcement_kepemilikan.json similarity index 100% rename from tests/fixtures/idx_announcement_kepemilikan.json rename to crates/idx-core/tests/fixtures/idx_announcement_kepemilikan.json diff --git a/tests/fixtures/idx_share_ownership_page_excerpt.html b/crates/idx-core/tests/fixtures/idx_share_ownership_page_excerpt.html similarity index 100% rename from tests/fixtures/idx_share_ownership_page_excerpt.html rename to crates/idx-core/tests/fixtures/idx_share_ownership_page_excerpt.html diff --git a/tests/fixtures/ksei_above1_20260831_excerpt.xlsx b/crates/idx-core/tests/fixtures/ksei_above1_20260831_excerpt.xlsx similarity index 100% rename from tests/fixtures/ksei_above1_20260831_excerpt.xlsx rename to crates/idx-core/tests/fixtures/ksei_above1_20260831_excerpt.xlsx diff --git a/tests/fixtures/ksei_above1_spaced_stext_excerpt.xml b/crates/idx-core/tests/fixtures/ksei_above1_spaced_stext_excerpt.xml similarity index 100% rename from tests/fixtures/ksei_above1_spaced_stext_excerpt.xml rename to crates/idx-core/tests/fixtures/ksei_above1_spaced_stext_excerpt.xml diff --git a/tests/fixtures/ksei_above1_stext_excerpt.xml b/crates/idx-core/tests/fixtures/ksei_above1_stext_excerpt.xml similarity index 100% rename from tests/fixtures/ksei_above1_stext_excerpt.xml rename to crates/idx-core/tests/fixtures/ksei_above1_stext_excerpt.xml diff --git a/tests/fixtures/ksei_above1_stext_excerpt_prev.xml b/crates/idx-core/tests/fixtures/ksei_above1_stext_excerpt_prev.xml similarity index 100% rename from tests/fixtures/ksei_above1_stext_excerpt_prev.xml rename to crates/idx-core/tests/fixtures/ksei_above1_stext_excerpt_prev.xml diff --git a/tests/fixtures/ksei_above5_20260923_excerpt.xlsx b/crates/idx-core/tests/fixtures/ksei_above5_20260923_excerpt.xlsx similarity index 100% rename from tests/fixtures/ksei_above5_20260923_excerpt.xlsx rename to crates/idx-core/tests/fixtures/ksei_above5_20260923_excerpt.xlsx diff --git a/tests/fixtures/ksei_above5_stext_excerpt.xml b/crates/idx-core/tests/fixtures/ksei_above5_stext_excerpt.xml similarity index 100% rename from tests/fixtures/ksei_above5_stext_excerpt.xml rename to crates/idx-core/tests/fixtures/ksei_above5_stext_excerpt.xml diff --git a/tests/fixtures/ksei_announcement_wrapper_stext_excerpt.xml b/crates/idx-core/tests/fixtures/ksei_announcement_wrapper_stext_excerpt.xml similarity index 100% rename from tests/fixtures/ksei_announcement_wrapper_stext_excerpt.xml rename to crates/idx-core/tests/fixtures/ksei_announcement_wrapper_stext_excerpt.xml diff --git a/tests/fixtures/ksei_balancepos_20260130_excerpt.txt b/crates/idx-core/tests/fixtures/ksei_balancepos_20260130_excerpt.txt similarity index 100% rename from tests/fixtures/ksei_balancepos_20260130_excerpt.txt rename to crates/idx-core/tests/fixtures/ksei_balancepos_20260130_excerpt.txt diff --git a/tests/fixtures/ksei_balancepos_20260227_excerpt.txt b/crates/idx-core/tests/fixtures/ksei_balancepos_20260227_excerpt.txt similarity index 100% rename from tests/fixtures/ksei_balancepos_20260227_excerpt.txt rename to crates/idx-core/tests/fixtures/ksei_balancepos_20260227_excerpt.txt diff --git a/tests/fixtures/ksei_investor_type_stext_excerpt.xml b/crates/idx-core/tests/fixtures/ksei_investor_type_stext_excerpt.xml similarity index 100% rename from tests/fixtures/ksei_investor_type_stext_excerpt.xml rename to crates/idx-core/tests/fixtures/ksei_investor_type_stext_excerpt.xml diff --git a/tests/fixtures/ksei_stext_sample.xml b/crates/idx-core/tests/fixtures/ksei_stext_sample.xml similarity index 100% rename from tests/fixtures/ksei_stext_sample.xml rename to crates/idx-core/tests/fixtures/ksei_stext_sample.xml diff --git a/tests/fixtures/msn_chart_bbca_3m.json b/crates/idx-core/tests/fixtures/msn_chart_bbca_3m.json similarity index 100% rename from tests/fixtures/msn_chart_bbca_3m.json rename to crates/idx-core/tests/fixtures/msn_chart_bbca_3m.json diff --git a/tests/fixtures/msn_chart_bbca_3mo.json b/crates/idx-core/tests/fixtures/msn_chart_bbca_3mo.json similarity index 100% rename from tests/fixtures/msn_chart_bbca_3mo.json rename to crates/idx-core/tests/fixtures/msn_chart_bbca_3mo.json diff --git a/tests/fixtures/msn_earnings_bbca.json b/crates/idx-core/tests/fixtures/msn_earnings_bbca.json similarity index 100% rename from tests/fixtures/msn_earnings_bbca.json rename to crates/idx-core/tests/fixtures/msn_earnings_bbca.json diff --git a/tests/fixtures/msn_financials_bbca.json b/crates/idx-core/tests/fixtures/msn_financials_bbca.json similarity index 100% rename from tests/fixtures/msn_financials_bbca.json rename to crates/idx-core/tests/fixtures/msn_financials_bbca.json diff --git a/tests/fixtures/msn_insights_bbca.json b/crates/idx-core/tests/fixtures/msn_insights_bbca.json similarity index 100% rename from tests/fixtures/msn_insights_bbca.json rename to crates/idx-core/tests/fixtures/msn_insights_bbca.json diff --git a/tests/fixtures/msn_keyratios_bbca.json b/crates/idx-core/tests/fixtures/msn_keyratios_bbca.json similarity index 100% rename from tests/fixtures/msn_keyratios_bbca.json rename to crates/idx-core/tests/fixtures/msn_keyratios_bbca.json diff --git a/tests/fixtures/msn_keyratios_industry_only.json b/crates/idx-core/tests/fixtures/msn_keyratios_industry_only.json similarity index 100% rename from tests/fixtures/msn_keyratios_industry_only.json rename to crates/idx-core/tests/fixtures/msn_keyratios_industry_only.json diff --git a/tests/fixtures/msn_keyratios_infinity.json b/crates/idx-core/tests/fixtures/msn_keyratios_infinity.json similarity index 100% rename from tests/fixtures/msn_keyratios_infinity.json rename to crates/idx-core/tests/fixtures/msn_keyratios_infinity.json diff --git a/tests/fixtures/msn_keyratios_negative_infinity.json b/crates/idx-core/tests/fixtures/msn_keyratios_negative_infinity.json similarity index 100% rename from tests/fixtures/msn_keyratios_negative_infinity.json rename to crates/idx-core/tests/fixtures/msn_keyratios_negative_infinity.json diff --git a/tests/fixtures/msn_news_bbca.json b/crates/idx-core/tests/fixtures/msn_news_bbca.json similarity index 100% rename from tests/fixtures/msn_news_bbca.json rename to crates/idx-core/tests/fixtures/msn_news_bbca.json diff --git a/tests/fixtures/msn_profile_bbca.json b/crates/idx-core/tests/fixtures/msn_profile_bbca.json similarity index 100% rename from tests/fixtures/msn_profile_bbca.json rename to crates/idx-core/tests/fixtures/msn_profile_bbca.json diff --git a/tests/fixtures/msn_quote_bbca.json b/crates/idx-core/tests/fixtures/msn_quote_bbca.json similarity index 100% rename from tests/fixtures/msn_quote_bbca.json rename to crates/idx-core/tests/fixtures/msn_quote_bbca.json diff --git a/tests/fixtures/msn_screener_id_topperfs.json b/crates/idx-core/tests/fixtures/msn_screener_id_topperfs.json similarity index 100% rename from tests/fixtures/msn_screener_id_topperfs.json rename to crates/idx-core/tests/fixtures/msn_screener_id_topperfs.json diff --git a/tests/fixtures/msn_sentiment_bbca.json b/crates/idx-core/tests/fixtures/msn_sentiment_bbca.json similarity index 100% rename from tests/fixtures/msn_sentiment_bbca.json rename to crates/idx-core/tests/fixtures/msn_sentiment_bbca.json diff --git a/tests/fixtures/quotesummary_bbca.json b/crates/idx-core/tests/fixtures/quotesummary_bbca.json similarity index 100% rename from tests/fixtures/quotesummary_bbca.json rename to crates/idx-core/tests/fixtures/quotesummary_bbca.json diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 030e959..7f6789e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,10 +1,25 @@ # Architecture +## Crates + +The repo is a Cargo workspace with one shared version: + +- `crates/idx-core` (library): providers (`api/`), `analysis/`, `cache`, config + file handling and provider selection (`config`), `ownership/` (SQLite, parsers, + discovery, snapshot sync), `error`, and `runtime` (quiet/info/warn). It owns the + shared test fixtures in `tests/fixtures/`. +- `crates/idx-cli` (binary `idx`): clap commands (`cli/`), output format and + color resolution (`config.rs`, `IdxConfig`), and rendering (`output/`). Its + `tests/` hold the CLI integration and golden output suites. + +Some fetch/cache orchestration still lives in `crates/idx-cli/src/cli/` until +phase 3 of `docs/DESIGN.md` moves it into idx-core. + ## Domain Map ``` ┌─────────────────────────────────────────────────────────┐ -│ CLI Layer │ +│ CLI Layer (idx-cli) │ │ main.rs → cli/stocks.rs, cli/ownership.rs, cli/... │ │ Clap derive structs, command dispatch, arg validation │ └──────────────────────┬──────────────────────────────────┘ @@ -39,7 +54,7 @@ sentiment, screener - `https://api.msn.com/msn/v0/pages/finance/`: key ratios, insights, news feed -The public API key embedded in MSN Money lives in `src/api/msn/client.rs`. +The public API key embedded in MSN Money lives in `crates/idx-core/src/api/msn/client.rs`. ### Hybrid History Strategy When `history_provider = auto` (default): @@ -89,7 +104,7 @@ Unlike the `stocks` module (live-fetch), ownership is **import-then-query**: - **KSEI** — official ≥1% shareholder registry, published monthly by IDX. Through May 2026 it was a PDF announcement; from the 2026-05-29 report onward it is an XLSX workbook on the IDX "Data Kepemilikan Saham" page - **KSEI archive** — monthly ZIP/TXT balance-position matrix, used as a local fallback/backstop import path - **Bing Finance** — global institutional ownership. The schema, `--source bing` - queries, and `src/api/msn/bing.rs` exist, but `ownership import --fetch-bing` + queries, and `crates/idx-core/src/api/msn/bing.rs` exist, but `ownership import --fetch-bing` is intentionally unsupported, so no ingest path populates Bing rows today ### Discovery diff --git a/docs/DESIGN.md b/docs/DESIGN.md index fbe9c3f..56615f8 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1,6 +1,6 @@ # System Design: idx-core and its frontends -**Status:** Accepted 2026-10-14. Implementation on `refactor/idx-core`; phases 0-1 done. +**Status:** Accepted 2026-10-14. Implementation on `refactor/idx-core`; phases 0-2 done. ## Goal @@ -99,7 +99,7 @@ Key decisions: - **Typed inputs.** Screener filters, regions, statement and earnings filters become enums with `FromStr`, so validation is identical across frontends. -## What Moves Out of `src/cli/` +## What Moves Out of `crates/idx-cli/src/cli/` | From | To | | --- | --- | diff --git a/flake.nix b/flake.nix index 7f5d170..c83465c 100644 --- a/flake.nix +++ b/flake.nix @@ -18,6 +18,7 @@ pkgs = import nixpkgs { inherit system overlays; }; lib = pkgs.lib; cargoManifest = builtins.fromTOML (builtins.readFile ./Cargo.toml); + version = cargoManifest.workspace.package.version; rustToolchain = pkgs.rust-bin.stable.latest.default.override { extensions = [ "rust-src" "rust-analyzer" ]; }; @@ -31,12 +32,15 @@ rustFiles = [ ./Cargo.toml ./Cargo.lock - ./src - ./tests/fixtures + ./crates/idx-core/Cargo.toml + ./crates/idx-core/src + ./crates/idx-core/tests/fixtures + ./crates/idx-cli/Cargo.toml + ./crates/idx-cli/src ]; testSrc = lib.fileset.toSource { root = ./.; - fileset = lib.fileset.unions (rustFiles ++ [ ./tests ]); + fileset = lib.fileset.unions (rustFiles ++ [ ./crates/idx-cli/tests ]); }; src = lib.fileset.toSource { root = ./.; @@ -44,8 +48,8 @@ }; commonArgs = { inherit src; - pname = cargoManifest.package.name; - version = cargoManifest.package.version; + pname = "idx-cli"; + inherit version; CARGO_PROFILE = "dev"; strictDeps = true; nativeBuildInputs = with pkgs; [ pkg-config ]; @@ -90,7 +94,7 @@ ]; ownershipPublisher = pkgs.stdenvNoCC.mkDerivation { pname = "idx-ownership-publisher"; - version = cargoManifest.package.version; + inherit version; src = lib.fileset.toSource { root = ./.; fileset = lib.fileset.unions publisherScripts; @@ -155,7 +159,7 @@ ${ownershipPublisher}/bin/idx-ownership-publish --help touch "$out" ''; - fmt = craneLib.cargoFmt { src = testSrc; }; + fmt = craneLib.cargoFmt { src = testSrc; pname = "idx-cli"; inherit version; }; clippy = craneLib.cargoClippy (commonArgs // { inherit cargoArtifacts; src = testSrc; @@ -172,20 +176,39 @@ # `curl` against a local fixture server. nativeCheckInputs = [ pkgs.curl ]; }); - # `cargo package` verification: the published crate (per the - # `include` list in Cargo.toml) must build on its own. + # `cargo package` verification: each published crate (per the + # `include` lists in crates/*/Cargo.toml) must build on its own. package = craneLib.mkCargoDerivation (commonArgs // { inherit cargoArtifacts; pnameSuffix = "-package"; src = lib.fileset.toSource { root = ./.; fileset = lib.fileset.unions (rustFiles ++ [ - ./tests + ./crates/idx-cli/tests + ./crates/idx-core/LICENSE + ./crates/idx-cli/LICENSE ./LICENSE ./README.md ]); }; - buildPhaseCargoCommand = "cargo package --locked --offline --allow-dirty"; + buildPhaseCargoCommand = '' + # idx-core: full `cargo package` verification. + cargo package -p idx-core --registry crates-io --locked --offline --allow-dirty + # idx-cli depends on idx-core, which is not on crates.io yet, so + # cargo cannot resolve it from the vendored registry. Package + # idx-cli without lockfile or verification, then build the + # extracted tarball against the packaged idx-core. Real releases + # publish idx-core first and package idx-cli normally. + cargo package -p idx-cli --registry crates-io --no-verify --exclude-lockfile --offline --allow-dirty + verify="$TMPDIR/package-verify" + mkdir -p "$verify" + tar -xzf target/package/idx-core-${version}.crate -C "$verify" + tar -xzf target/package/idx-cli-${version}.crate -C "$verify" + cp Cargo.lock "$verify/idx-cli-${version}/" + (cd "$verify/idx-cli-${version}" && cargo build --offline \ + --config 'patch.crates-io.idx-core.path="../idx-core-${version}"' \ + --target-dir "$verify/target") + ''; doInstallCargoArtifacts = false; installPhaseCommand = "touch $out"; }); @@ -196,7 +219,7 @@ '' # The mock provider reads fixtures relative to the working dir. mkdir -p tests - cp -r ${./tests/fixtures} tests/fixtures + cp -r ${./crates/idx-core/tests/fixtures} tests/fixtures export HOME="$TMPDIR" ${idxPackage}/bin/idx -o json version | jq -e '.version | type == "string"' ${idxPackage}/bin/idx completions bash | bash -n