diff --git a/cli/search/register.ts b/cli/search/register.ts index 38433a4d1..b9aa93e1d 100644 --- a/cli/search/register.ts +++ b/cli/search/register.ts @@ -4,6 +4,24 @@ import Fuse from "fuse.js" import kleur from "kleur" import { getQueryFromParts } from "cli/utils/get-query-from-parts" +export type MouserSearchResult = { + mouser_product_number: string + supplier_part_number?: string + mfr: string + manufacturer?: string + package?: string + description: string + stock: number + price?: number + product_url?: string +} + +export const formatMouserSearchResult = ( + component: MouserSearchResult, + index: number, +): string => + `${index + 1}. ${component.mfr} (${component.mouser_product_number}) - ${component.description} (stock: ${component.stock.toLocaleString("en-US")})` + export const registerSearch = (program: Command) => { program .command("search") @@ -17,6 +35,7 @@ export const registerSearch = (program: Command) => { .option("--kicad", "Search KiCad footprints") .option("--jlcpcb", "Search JLCPCB components") .option("--lcsc", "Alias for --jlcpcb") + .option("--mouser", "Search Mouser components") .option("--tscircuit", "Search tscircuit registry packages") .option("--json", "Output search results as JSON") .action( @@ -26,15 +45,21 @@ export const registerSearch = (program: Command) => { kicad?: boolean jlcpcb?: boolean lcsc?: boolean + mouser?: boolean tscircuit?: boolean json?: boolean }, ) => { const query = getQueryFromParts(queryParts) const hasFilters = - opts.kicad || opts.jlcpcb || opts.lcsc || opts.tscircuit + opts.kicad || + opts.jlcpcb || + opts.lcsc || + opts.mouser || + opts.tscircuit const searchKicad = opts.kicad const searchJlc = opts.jlcpcb || opts.lcsc || !hasFilters + const searchMouser = opts.mouser const searchTscircuit = opts.tscircuit let results: { @@ -55,6 +80,8 @@ export const registerSearch = (program: Command) => { price: number }> = [] + let mouserResults: MouserSearchResult[] = [] + let kicadResults: string[] = [] try { @@ -75,6 +102,22 @@ export const registerSearch = (program: Command) => { jlcResults = jlcResponse?.components ?? [] } + if (searchMouser) { + const mouserSearchUrl = + "https://mousersearch.tscircuit.com/api/search?limit=10&q=" + + encodeURIComponent(query) + const mouserResponse = await fetch(mouserSearchUrl, { + headers: { accept: "application/json" }, + }) + if (!mouserResponse.ok) { + throw new Error( + `Mouser search returned ${mouserResponse.status}: ${await mouserResponse.text()}`, + ) + } + const mouserJson = await mouserResponse.json() + mouserResults = mouserJson?.components ?? [] + } + if (searchKicad) { const kicadFiles: string[] = await fetch( "https://kicad-mod-cache.tscircuit.com/kicad_files.json", @@ -107,6 +150,10 @@ export const registerSearch = (program: Command) => { source: "jlcpcb" as const, ...comp, })), + ...mouserResults.map((comp) => ({ + source: "mouser" as const, + ...comp, + })), ] console.log( @@ -125,11 +172,13 @@ export const registerSearch = (program: Command) => { if ( !kicadResults.length && !results.packages.length && - !jlcResults.length + !jlcResults.length && + !mouserResults.length ) { const sources = [ searchTscircuit && "tscircuit registry", searchJlc && "JLCPCB", + searchMouser && "Mouser", searchKicad && "KiCad", ].filter(Boolean) console.log( @@ -194,6 +243,21 @@ export const registerSearch = (program: Command) => { ) }) } + + if (mouserResults.length) { + console.log() + console.log( + kleur + .bold() + .underline( + `Found ${mouserResults.length} component(s) in Mouser search:`, + ), + ) + + mouserResults.forEach((component, index) => { + console.log(formatMouserSearchResult(component, index)) + }) + } console.log("\n") }, ) diff --git a/tests/cli/search/search-mouser.test.ts b/tests/cli/search/search-mouser.test.ts new file mode 100644 index 000000000..c9861becd --- /dev/null +++ b/tests/cli/search/search-mouser.test.ts @@ -0,0 +1,68 @@ +import { afterEach, describe, expect, it, spyOn } from "bun:test" +import { Command } from "commander" +import { registerSearch } from "../../../cli/search/register" + +const originalFetch = globalThis.fetch + +afterEach(() => { + globalThis.fetch = originalFetch +}) + +describe("search --mouser", () => { + it("queries mousersearch and emits unified JSON results", async () => { + let requestedUrl = "" + globalThis.fetch = (async (input) => { + requestedUrl = String(input) + return new Response( + JSON.stringify({ + components: [ + { + mouser_product_number: "603-RC0603FR-0710KL", + mfr: "RC0603FR-0710KL", + manufacturer: "YAGEO", + package: "0603 (1608 Metric)", + description: "Thick Film Resistors - SMD 10K ohm 1%", + stock: 1200, + price: 0.01, + }, + ], + }), + { status: 200 }, + ) + }) as typeof fetch + + const log = spyOn(console, "log").mockImplementation(() => {}) + const program = new Command().name("tsci") + registerSearch(program) + + await program.parseAsync([ + "node", + "tsci", + "search", + "--mouser", + "--json", + "10k", + "0603", + ]) + + expect(requestedUrl).toBe( + "https://mousersearch.tscircuit.com/api/search?limit=10&q=10k%200603", + ) + const output = String(log.mock.calls[0]?.[0]) + const parsed = JSON.parse(output) + expect(parsed.query).toBe("10k 0603") + expect(parsed.results).toEqual([ + { + source: "mouser", + mouser_product_number: "603-RC0603FR-0710KL", + mfr: "RC0603FR-0710KL", + manufacturer: "YAGEO", + package: "0603 (1608 Metric)", + description: "Thick Film Resistors - SMD 10K ohm 1%", + stock: 1200, + price: 0.01, + }, + ]) + log.mockRestore() + }) +})