@jlcpcb/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,74 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { createComponentService, type ComponentSearchResult } from '@jlcpcb/core';
4
+ import { useNavigation, useCurrentScreen } from '../navigation/NavigationContext.js';
5
+ import type { SearchParams } from '../navigation/types.js';
6
+ import { useAppState } from '../state/AppStateContext.js';
7
+ import { useTerminalSize } from '../hooks/useTerminalSize.js';
8
+ import { ListView } from '../components/ListView.js';
9
+
10
+ const componentService = createComponentService();
11
+
12
+ export function SearchScreen() {
13
+ const { push } = useNavigation();
14
+ const { params } = useCurrentScreen() as { screen: 'search'; params: SearchParams };
15
+ const { selectedIndex, setSelectedIndex, isFiltered, setIsFiltered, resetSelection } = useAppState();
16
+ const { columns: terminalWidth } = useTerminalSize();
17
+
18
+ const [results, setResults] = useState<ComponentSearchResult[]>(params.results);
19
+ const [isSearching, setIsSearching] = useState(false);
20
+
21
+ useInput(async (input, key) => {
22
+ if (isSearching) return;
23
+
24
+ if (key.upArrow) {
25
+ setSelectedIndex(Math.max(0, selectedIndex - 1));
26
+ } else if (key.downArrow) {
27
+ setSelectedIndex(Math.min(results.length - 1, selectedIndex + 1));
28
+ } else if (key.return && results[selectedIndex]) {
29
+ push('info', {
30
+ componentId: results[selectedIndex].lcscId,
31
+ component: results[selectedIndex],
32
+ });
33
+ } else if (key.tab) {
34
+ setIsSearching(true);
35
+ const newFiltered = !isFiltered;
36
+ try {
37
+ let newResults = await componentService.search(params.query, {
38
+ limit: 20,
39
+ basicOnly: newFiltered,
40
+ });
41
+ newResults = newResults.sort((a, b) => {
42
+ if (a.libraryType === 'basic' && b.libraryType !== 'basic') return -1;
43
+ if (a.libraryType !== 'basic' && b.libraryType === 'basic') return 1;
44
+ return 0;
45
+ });
46
+ setResults(newResults);
47
+ resetSelection();
48
+ setIsFiltered(newFiltered);
49
+ } catch {
50
+ // Keep existing results on error
51
+ }
52
+ setIsSearching(false);
53
+ }
54
+ });
55
+
56
+ return (
57
+ <Box flexDirection="column">
58
+ <Box marginBottom={1}>
59
+ <Text bold>
60
+ Search: <Text color="cyan">{params.query}</Text>
61
+ {' '}
62
+ <Text dimColor>({results.length} results{isFiltered ? ' - Basic/Preferred only' : ''})</Text>
63
+ {isSearching && <Text color="yellow"> ⏳</Text>}
64
+ </Text>
65
+ </Box>
66
+ <ListView
67
+ results={results}
68
+ selectedIndex={selectedIndex}
69
+ isFiltered={isFiltered}
70
+ terminalWidth={terminalWidth}
71
+ />
72
+ </Box>
73
+ );
74
+ }
@@ -0,0 +1,64 @@
1
+ import React, { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
2
+
3
+ export interface AppState {
4
+ selectedIndex: number;
5
+ isFiltered: boolean;
6
+ isLoading: boolean;
7
+ }
8
+
9
+ export interface AppStateActions {
10
+ setSelectedIndex: (index: number) => void;
11
+ setIsFiltered: (filtered: boolean) => void;
12
+ setIsLoading: (loading: boolean) => void;
13
+ resetSelection: () => void;
14
+ }
15
+
16
+ export type AppStateContextValue = AppState & AppStateActions;
17
+
18
+ const AppStateContext = createContext<AppStateContextValue | null>(null);
19
+
20
+ interface AppStateProviderProps {
21
+ children: ReactNode;
22
+ }
23
+
24
+ export function AppStateProvider({ children }: AppStateProviderProps) {
25
+ const [selectedIndex, setSelectedIndexState] = useState(0);
26
+ const [isFiltered, setIsFilteredState] = useState(false);
27
+ const [isLoading, setIsLoadingState] = useState(false);
28
+
29
+ const setSelectedIndex = useCallback((index: number) => {
30
+ setSelectedIndexState(index);
31
+ }, []);
32
+
33
+ const setIsFiltered = useCallback((filtered: boolean) => {
34
+ setIsFilteredState(filtered);
35
+ }, []);
36
+
37
+ const setIsLoading = useCallback((loading: boolean) => {
38
+ setIsLoadingState(loading);
39
+ }, []);
40
+
41
+ const resetSelection = useCallback(() => {
42
+ setSelectedIndexState(0);
43
+ }, []);
44
+
45
+ const value: AppStateContextValue = {
46
+ selectedIndex,
47
+ isFiltered,
48
+ isLoading,
49
+ setSelectedIndex,
50
+ setIsFiltered,
51
+ setIsLoading,
52
+ resetSelection,
53
+ };
54
+
55
+ return <AppStateContext.Provider value={value}>{children}</AppStateContext.Provider>;
56
+ }
57
+
58
+ export function useAppState(): AppStateContextValue {
59
+ const context = useContext(AppStateContext);
60
+ if (!context) {
61
+ throw new Error('useAppState must be used within AppStateProvider');
62
+ }
63
+ return context;
64
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Info command
3
+ * Display component details
4
+ */
5
+
6
+ import * as p from '@clack/prompts';
7
+ import chalk from 'chalk';
8
+ import { createComponentService } from '@jlcpcb/core';
9
+ import { renderApp } from '../app/App.js';
10
+
11
+ const componentService = createComponentService();
12
+
13
+ interface InfoOptions {
14
+ json?: boolean;
15
+ }
16
+
17
+ export async function infoCommand(id: string, options: InfoOptions): Promise<void> {
18
+ // JSON mode - non-interactive output for scripting
19
+ if (options.json) {
20
+ const spinner = p.spinner();
21
+ spinner.start(`Fetching component ${id}...`);
22
+
23
+ try {
24
+ const details = await componentService.getDetails(id);
25
+ spinner.stop('Component found');
26
+ console.log(JSON.stringify(details, null, 2));
27
+ } catch (error) {
28
+ spinner.stop('Failed to fetch component');
29
+ p.log.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
30
+ process.exit(1);
31
+ }
32
+ return;
33
+ }
34
+
35
+ // Interactive mode - launch TUI
36
+ renderApp('info', { componentId: id });
37
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Install command
3
+ * Fetch component and add to KiCad libraries
4
+ */
5
+
6
+ import * as p from '@clack/prompts';
7
+ import chalk from 'chalk';
8
+ import { createComponentService, createLibraryService, type SearchOptions } from '@jlcpcb/core';
9
+ import { renderApp } from '../app/App.js';
10
+
11
+ const componentService = createComponentService();
12
+ const libraryService = createLibraryService();
13
+
14
+ interface InstallOptions {
15
+ projectPath?: string;
16
+ include3d?: boolean;
17
+ force?: boolean;
18
+ }
19
+
20
+ export async function installCommand(id: string | undefined, options: InstallOptions): Promise<void> {
21
+ // If ID provided with --force, do direct install (non-interactive)
22
+ if (id && options.force) {
23
+ const spinner = p.spinner();
24
+ spinner.start(`Installing component ${id}...`);
25
+
26
+ try {
27
+ // Ensure libraries are set up
28
+ await libraryService.ensureGlobalTables();
29
+
30
+ // Install the component
31
+ const result = await libraryService.install(id, {
32
+ projectPath: options.projectPath,
33
+ include3d: options.include3d,
34
+ force: true,
35
+ });
36
+
37
+ spinner.stop(chalk.green('✓ Component installed'));
38
+
39
+ // Display result
40
+ console.log();
41
+ console.log(chalk.cyan('Symbol: '), result.symbolRef);
42
+ console.log(chalk.cyan('Footprint: '), result.footprintRef);
43
+ console.log(chalk.cyan('Action: '), result.symbolAction);
44
+ if (result.files.model3d) {
45
+ console.log(chalk.cyan('3D Model: '), result.files.model3d);
46
+ }
47
+ console.log();
48
+ console.log(chalk.dim(`Library: ${result.files.symbolLibrary}`));
49
+ } catch (error) {
50
+ spinner.stop(chalk.red('✗ Installation failed'));
51
+ p.log.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
52
+ process.exit(1);
53
+ }
54
+ return;
55
+ }
56
+
57
+ // If ID provided (without --force), fetch component and launch TUI for install
58
+ if (id) {
59
+ const spinner = p.spinner();
60
+ spinner.start(`Fetching component ${id}...`);
61
+
62
+ try {
63
+ const details = await componentService.getDetails(id);
64
+ spinner.stop('Component found');
65
+
66
+ // Launch TUI at info screen (user can navigate to install from there)
67
+ renderApp('info', { componentId: id, component: details as any });
68
+ } catch (error) {
69
+ spinner.stop('Failed to fetch component');
70
+ p.log.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
71
+ process.exit(1);
72
+ }
73
+ return;
74
+ }
75
+
76
+ // No ID provided - interactive search mode using @clack/prompts
77
+ const query = await p.text({
78
+ message: 'Search for component:',
79
+ placeholder: 'e.g., STM32F103, ESP32, 10k resistor',
80
+ validate: (value) => {
81
+ if (!value) return 'Please enter a search term';
82
+ return undefined;
83
+ },
84
+ });
85
+
86
+ if (p.isCancel(query)) {
87
+ p.cancel('Installation cancelled');
88
+ process.exit(0);
89
+ }
90
+
91
+ const spinner = p.spinner();
92
+ spinner.start(`Searching for "${query}"...`);
93
+
94
+ const searchOptions: SearchOptions = { limit: 20 };
95
+ let results = await componentService.search(query as string, searchOptions);
96
+
97
+ // Sort results: basic parts first
98
+ results = results.sort((a, b) => {
99
+ if (a.libraryType === 'basic' && b.libraryType !== 'basic') return -1;
100
+ if (a.libraryType !== 'basic' && b.libraryType === 'basic') return 1;
101
+ return 0;
102
+ });
103
+
104
+ spinner.stop(`Found ${results.length} results`);
105
+
106
+ if (results.length === 0) {
107
+ p.log.warn('No components found. Try a different search term.');
108
+ return;
109
+ }
110
+
111
+ // Launch TUI for selection and install
112
+ renderApp('search', { query: query as string, results });
113
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Library command
3
+ * View JLC-MCP library status and installed components
4
+ */
5
+
6
+ import * as p from '@clack/prompts';
7
+ import { createLibraryService } from '@jlcpcb/core';
8
+ import { renderApp } from '../app/App.js';
9
+
10
+ const libraryService = createLibraryService();
11
+
12
+ interface LibraryOptions {
13
+ json?: boolean;
14
+ }
15
+
16
+ export async function libraryCommand(options: LibraryOptions): Promise<void> {
17
+ // JSON mode - non-interactive output for scripting
18
+ if (options.json) {
19
+ const spinner = p.spinner();
20
+ spinner.start('Loading library status...');
21
+
22
+ try {
23
+ const [status, components] = await Promise.all([
24
+ libraryService.getStatus(),
25
+ libraryService.listInstalled({}),
26
+ ]);
27
+
28
+ spinner.stop(`Found ${components.length} installed components`);
29
+
30
+ console.log(
31
+ JSON.stringify(
32
+ {
33
+ status: {
34
+ installed: status.installed,
35
+ linked: status.linked,
36
+ version: status.version,
37
+ componentCount: status.componentCount,
38
+ paths: status.paths,
39
+ },
40
+ components,
41
+ },
42
+ null,
43
+ 2
44
+ )
45
+ );
46
+ } catch (error) {
47
+ spinner.stop('Failed to get library status');
48
+ p.log.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
49
+ process.exit(1);
50
+ }
51
+ return;
52
+ }
53
+
54
+ // Interactive mode - launch TUI
55
+ renderApp('library', {});
56
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Search command
3
+ * Interactive search for components from LCSC or EasyEDA community
4
+ */
5
+
6
+ import { createComponentService, type SearchOptions } from '@jlcpcb/core';
7
+ import { renderApp } from '../app/App.js';
8
+
9
+ const componentService = createComponentService();
10
+
11
+ export async function searchCommand(query: string, options: SearchOptions): Promise<void> {
12
+ console.log(`Searching for "${query}"...`);
13
+
14
+ try {
15
+ let results = await componentService.search(query, options);
16
+
17
+ // Sort results: basic parts first, then extended
18
+ if (!options.basicOnly) {
19
+ results = results.sort((a, b) => {
20
+ if (a.libraryType === 'basic' && b.libraryType !== 'basic') return -1;
21
+ if (a.libraryType !== 'basic' && b.libraryType === 'basic') return 1;
22
+ return 0;
23
+ });
24
+ }
25
+
26
+ if (results.length === 0) {
27
+ console.log('No components found. Try a different search term.');
28
+ return;
29
+ }
30
+
31
+ // Clear the "Searching..." line and launch interactive UI
32
+ process.stdout.write('\x1b[1A\x1b[2K');
33
+ renderApp('search', { query, results });
34
+ } catch (error) {
35
+ console.error(`Search failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
36
+ process.exit(1);
37
+ }
38
+ }
package/src/index.ts ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * jlc-cli
5
+ * CLI for JLC/EasyEDA component sourcing and KiCad library management
6
+ */
7
+
8
+ import { Command } from 'commander';
9
+ import { searchCommand } from './commands/search.js';
10
+ import { infoCommand } from './commands/info.js';
11
+ import { installCommand } from './commands/install.js';
12
+ import { libraryCommand } from './commands/library.js';
13
+
14
+ const program = new Command();
15
+
16
+ program
17
+ .name('jlc')
18
+ .description('JLC/EasyEDA component sourcing and KiCad library management')
19
+ .version('0.1.0');
20
+
21
+ program
22
+ .command('search <query...>')
23
+ .description('Search for components (basic parts sorted first)')
24
+ .option('-l, --limit <number>', 'Maximum results', '20')
25
+ .option('--in-stock', 'Only show in-stock components')
26
+ .option('--basic-only', 'Only show basic parts (no extended)')
27
+ .option('--community', 'Search EasyEDA community library')
28
+ .action(async (queryParts: string[], options) => {
29
+ const query = queryParts.join(' ');
30
+ await searchCommand(query, {
31
+ limit: parseInt(options.limit, 10),
32
+ inStock: options.inStock,
33
+ basicOnly: options.basicOnly || false,
34
+ source: options.community ? 'easyeda-community' : 'lcsc',
35
+ });
36
+ });
37
+
38
+ program
39
+ .command('info <id>')
40
+ .description('Get component details')
41
+ .option('--json', 'Output as JSON')
42
+ .action(async (id, options) => {
43
+ await infoCommand(id, { json: options.json });
44
+ });
45
+
46
+ program
47
+ .command('install [id]')
48
+ .description('Install component to KiCad libraries')
49
+ .option('-p, --project <path>', 'Install to project-local library')
50
+ .option('--with-3d', 'Include 3D model')
51
+ .option('-f, --force', 'Force reinstall (regenerate symbol and footprint)')
52
+ .action(async (id, options) => {
53
+ await installCommand(id, {
54
+ projectPath: options.project,
55
+ include3d: options.with3d,
56
+ force: options.force,
57
+ });
58
+ });
59
+
60
+ program
61
+ .command('library')
62
+ .description('View JLC-MCP library status and installed components')
63
+ .option('--json', 'Output as JSON')
64
+ .action(async (options) => {
65
+ await libraryCommand({
66
+ json: options.json,
67
+ });
68
+ });
69
+
70
+ program.parse();
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "jsx": "react-jsx",
7
+ "jsxImportSource": "react"
8
+ },
9
+ "include": ["src/**/*"]
10
+ }