app
JLCPCB Parts Library
Public Made by Adomby adom
Check the JLCPCB / LCSC parts library before you fab — Basic vs Extended tier, LCSC stock and assembly pricing, so you avoid surprise feeder-setup fees. CLI, a Hydrogen app, and a shared backend.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
//! adom-jlcpcb: JLCPCB component search.
//!
//! Thin Rust CLI over the existing Node.js JLCPCB backend. Five verbs
//! matching the legacy MCP tools:
//! search — keyword/part-number search (FTS5)
//! category-search — keyword search scoped to a category
//! list — list components in a category, sorted by stock
//! categories — list all categories + subcategories
//! health — backend reachability + DB stats
mod app;
mod cli;
mod install;
mod jlcpcb;
use clap::{CommandFactory, Parser, Subcommand};
use std::io::IsTerminal;
use std::process::ExitCode;
use std::sync::OnceLock;
pub const GREEN: &str = "\x1b[32m";
pub const RED: &str = "\x1b[31m";
pub const YELLOW: &str = "\x1b[33m";
pub const CYAN: &str = "\x1b[36m";
pub const BOLD: &str = "\x1b[1m";
pub const DIM: &str = "\x1b[2m";
pub const RESET: &str = "\x1b[0m";
static STDOUT_IS_TTY: OnceLock<bool> = OnceLock::new();
static STDERR_IS_TTY: OnceLock<bool> = OnceLock::new();
pub fn stdout_is_tty() -> bool { *STDOUT_IS_TTY.get_or_init(|| std::io::stdout().is_terminal()) }
pub fn stderr_is_tty() -> bool { *STDERR_IS_TTY.get_or_init(|| std::io::stderr().is_terminal()) }
pub fn strip_ansi(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\x1b' && chars.peek() == Some(&'[') {
chars.next();
for c2 in chars.by_ref() { if c2.is_ascii_alphabetic() { break; } }
continue;
}
out.push(c);
}
out
}
pub fn ok(msg: impl std::fmt::Display) {
if stdout_is_tty() { println!("{GREEN}{BOLD}OK:{RESET} {msg}"); }
else { println!("OK: {}", strip_ansi(&msg.to_string())); }
}
pub fn err(msg: impl std::fmt::Display) {
if stderr_is_tty() { eprintln!("{RED}{BOLD}ERROR:{RESET} {msg}"); }
else { eprintln!("ERROR: {}", strip_ansi(&msg.to_string())); }
}
pub fn hint(msg: impl std::fmt::Display) {
if stderr_is_tty() { eprintln!("{DIM}Hint:{RESET} {msg}"); }
else { eprintln!("Hint: {}", strip_ansi(&msg.to_string())); }
}
pub fn warn(msg: impl std::fmt::Display) {
if stderr_is_tty() { eprintln!("{YELLOW}{BOLD}WARN:{RESET} {msg}"); }
else { eprintln!("WARN: {}", strip_ansi(&msg.to_string())); }
}
#[macro_export]
macro_rules! note {
($($arg:tt)*) => {{
let __s = format!($($arg)*);
if $crate::stdout_is_tty() { println!("{}", __s); }
else { println!("{}", $crate::strip_ansi(&__s)); }
}};
}
#[derive(Parser)]
#[command(name = "adom-jlcpcb", version, about = "JLCPCB component search — CLI + Hydrogen webview app")]
pub struct Cli {
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
/// Keyword / part-number search via FTS5. Backend is Node + SQLite.
Search(SearchArgs),
/// Keyword search scoped to a specific category.
#[command(name = "category-search")]
CategorySearch(CategorySearchArgs),
/// List all components in a category, sorted by stock.
List(ListArgs),
/// List all categories and subcategories.
Categories(CategoriesArgs),
/// Backend reachability + DB stats.
Health(HealthArgs),
/// Open the JLCPCB search webview as a Hydrogen tab.
App(AppArgs),
/// Install SKILL.md + BUILD-SKILL.md + bash completions.
Install,
/// Print shell completion script.
Completions { shell: clap_complete::Shell },
}
#[derive(clap::Args)]
pub struct SearchArgs {
pub keyword: String,
#[arg(long, default_value_t = 10)] pub limit: u32,
#[arg(long)] pub in_stock_only: bool,
#[arg(long, env = "JLCPCB_API")] pub api: Option<String>,
}
#[derive(clap::Args)]
pub struct CategorySearchArgs {
pub keyword: String,
#[arg(long)] pub category: String,
#[arg(long, default_value_t = 10)] pub limit: u32,
#[arg(long)] pub in_stock_only: bool,
#[arg(long, env = "JLCPCB_API")] pub api: Option<String>,
}
#[derive(clap::Args)]
pub struct ListArgs {
#[arg(long)] pub category: String,
#[arg(long)] pub subcategory: Option<String>,
#[arg(long, default_value_t = 10)] pub limit: u32,
#[arg(long)] pub in_stock_only: bool,
#[arg(long, env = "JLCPCB_API")] pub api: Option<String>,
}
#[derive(clap::Args)]
pub struct CategoriesArgs {
#[arg(long, env = "JLCPCB_API")] pub api: Option<String>,
}
#[derive(clap::Args)]
pub struct HealthArgs {
#[arg(long, env = "JLCPCB_API")] pub api: Option<String>,
}
#[derive(clap::Args)]
pub struct AppArgs {
#[arg(long, default_value_t = 8892)] pub port: u16,
#[arg(long, env = "JLCPCB_API")] pub api: Option<String>,
#[arg(long, default_value = "JLCPCB Search")] pub tab_name: String,
#[arg(long)] pub no_tab: bool,
#[arg(long, env = "ADOM_JLCPCB_DEV")] pub dev: bool,
}
fn main() -> ExitCode {
let args = Cli::parse();
let result = match args.cmd {
Cmd::Search(a) => cli::run_search(a),
Cmd::CategorySearch(a) => cli::run_category_search(a),
Cmd::List(a) => cli::run_list(a),
Cmd::Categories(a) => cli::run_categories(a),
Cmd::Health(a) => cli::run_health(a),
Cmd::App(a) => app::run(a),
Cmd::Install => install::install(),
Cmd::Completions { shell } => {
let mut cmd = Cli::command();
clap_complete::generate(shell, &mut cmd, "adom-jlcpcb", &mut std::io::stdout());
Ok(())
}
};
match result {
Ok(()) => ExitCode::SUCCESS,
Err(e) => { err(&e); ExitCode::FAILURE }
}
}