app
Adom Parts Search
Public Made by Adomby adom
One search box across Mouser + DigiKey + JLCPCB. Parallel queries, side-by-side product photos, and a Mouser-preferred recommendation (40-min drone delivery to Fort Worth) that reasons around stock and lead time.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
//! adom-parts-search: unified parts search across Mouser + DigiKey + JLCPCB.
//!
//! The three vendor CLIs (adom-mouser / adom-digikey / adom-jlcpcb) remain
//! the atomic primitives. This app's job is coordination: fire 3 parallel
//! queries, normalize into a unified schema, render a side-by-side grid
//! with product photos (which you can't see in a terminal), and apply the
//! Mouser-preferred routing policy from the `adom-parts-search` skill.
//!
//! Client-only — no service container. Talks to the three existing
//! vendor backends over HTTPS.
mod app;
mod cli;
mod install;
mod vendors;
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(m: impl std::fmt::Display) {
if stdout_is_tty() { println!("{GREEN}{BOLD}OK:{RESET} {m}"); } else { println!("OK: {}", strip_ansi(&m.to_string())); }
}
pub fn err(m: impl std::fmt::Display) {
if stderr_is_tty() { eprintln!("{RED}{BOLD}ERROR:{RESET} {m}"); } else { eprintln!("ERROR: {}", strip_ansi(&m.to_string())); }
}
pub fn warn(m: impl std::fmt::Display) {
if stderr_is_tty() { eprintln!("{YELLOW}{BOLD}WARN:{RESET} {m}"); } else { eprintln!("WARN: {}", strip_ansi(&m.to_string())); }
}
pub fn hint(m: impl std::fmt::Display) {
if stderr_is_tty() { eprintln!("{DIM}Hint:{RESET} {m}"); } else { eprintln!("Hint: {}", strip_ansi(&m.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-parts-search", version, about = "Unified parts search across Mouser/DigiKey/JLCPCB with Mouser-preferred routing (Fort Worth 40-min drone delivery)")]
pub struct Cli {
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
/// Search all three vendors in parallel; print unified JSON to stdout.
Search(SearchArgs),
/// Open the Hydrogen webview — 3-column grid with product photos.
App(AppArgs),
/// DEPRECATED — this feature moved to the `adom-chipfit` app.
/// See `adom-parts-search align --help` for the one-liner.
#[command(hide = false)]
Align(AlignArgs),
/// Run a search AND push the results into the running webview app so
/// the user sees the 3-column grid. Prints the full vendor-by-vendor
/// result summary to stdout so the AI (and user) both see the same
/// data. Auto-starts the app server + opens the tab if not already up.
Show(ShowArgs),
/// Install 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 = 5)] pub limit: u32,
#[arg(long)] pub in_stock_only: bool,
#[arg(long, env = "MOUSER_API")] pub mouser_api: Option<String>,
#[arg(long, env = "DIGIKEY_API")] pub digikey_api: Option<String>,
#[arg(long, env = "JLCPCB_API")] pub jlcpcb_api: Option<String>,
}
#[derive(clap::Args)]
pub struct ShowArgs {
pub keyword: String,
#[arg(long, default_value_t = 5)] pub limit: u32,
#[arg(long)] pub in_stock_only: bool,
#[arg(long, default_value_t = 8893)] pub port: u16,
#[arg(long, default_value = "Parts Search")] pub tab_name: String,
}
#[derive(clap::Args)]
pub struct AlignArgs {
/// Path to a `.kicad_mod` footprint file.
#[arg(long)]
pub footprint: std::path::PathBuf,
/// Path to a `.glb` model. If you have a STEP, convert first via
/// `adom-molecule create --step file.step` (or export a .glb from
/// Fusion / KiCad / your existing pipeline).
#[arg(long)]
pub glb: std::path::PathBuf,
/// HTTP port for the align server. Kept distinct from `app` (8893).
#[arg(long, default_value_t = 8894)]
pub port: u16,
/// Tab name in the Hydrogen workspace.
#[arg(long, default_value = "Part Align")]
pub tab_name: String,
/// Don't open the Hydrogen tab (just run the server — useful for
/// capturing via pup or curling endpoints directly).
#[arg(long)]
pub no_tab: bool,
}
#[derive(clap::Args)]
pub struct AppArgs {
#[arg(long, default_value_t = 8893)] pub port: u16,
#[arg(long, default_value = "Parts Search")] pub tab_name: String,
#[arg(long)] pub no_tab: bool,
#[arg(long, env = "MOUSER_API")] pub mouser_api: Option<String>,
#[arg(long, env = "DIGIKEY_API")] pub digikey_api: Option<String>,
#[arg(long, env = "JLCPCB_API")] pub jlcpcb_api: Option<String>,
#[arg(long, env = "ADOM_PARTS_SEARCH_DEV")] pub dev: bool,
}
fn main() -> ExitCode {
let args = Cli::parse();
let result = match args.cmd {
Cmd::Search(a) => cli::run_search(a),
Cmd::Show(a) => cli::run_show(a),
Cmd::App(a) => app::run(a),
Cmd::Align(_) => {
warn("`adom-parts-search align` has moved to its own app: `adom-chipfit`.");
note!("");
note!(" Install it (one-liner, run once per container):");
note!(" {CYAN}curl -fsSL https://wiki-ufypy5dpx93o.adom.cloud/static/apps/adom-chipfit/adom-chipfit \\");
note!(" -o /tmp/adom-chipfit && chmod +x /tmp/adom-chipfit \\");
note!(" && sudo install -m 0755 /tmp/adom-chipfit /usr/local/bin/adom-chipfit \\");
note!(" && adom-chipfit install{RESET}");
note!("");
note!(" Then run the same validation via:");
note!(" {CYAN}adom-chipfit check --footprint <fp.kicad_mod> --glb <chip.glb>{RESET}");
Err("align has been removed from adom-parts-search — see message above".into())
}
Cmd::Install => install::install(),
Cmd::Completions { shell } => {
let mut cmd = Cli::command();
clap_complete::generate(shell, &mut cmd, "adom-parts-search", &mut std::io::stdout());
Ok(())
}
};
match result { Ok(()) => ExitCode::SUCCESS, Err(e) => { err(&e); ExitCode::FAILURE } }
}