@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.
- package/CHANGELOG.md +15 -0
- package/README.md +251 -0
- package/dist/index.js +51677 -0
- package/package.json +53 -0
- package/src/app/App.tsx +85 -0
- package/src/app/components/DetailView.tsx +173 -0
- package/src/app/components/Divider.tsx +10 -0
- package/src/app/components/InstalledView.tsx +70 -0
- package/src/app/components/ListView.tsx +93 -0
- package/src/app/hooks/useTerminalSize.ts +29 -0
- package/src/app/navigation/NavigationContext.tsx +87 -0
- package/src/app/navigation/types.ts +66 -0
- package/src/app/screens/InfoScreen.tsx +203 -0
- package/src/app/screens/InstallScreen.tsx +54 -0
- package/src/app/screens/InstalledScreen.tsx +39 -0
- package/src/app/screens/LibraryScreen.tsx +259 -0
- package/src/app/screens/LibrarySetupScreen.tsx +120 -0
- package/src/app/screens/SearchScreen.tsx +74 -0
- package/src/app/state/AppStateContext.tsx +64 -0
- package/src/commands/info.ts +37 -0
- package/src/commands/install.ts +113 -0
- package/src/commands/library.ts +56 -0
- package/src/commands/search.ts +38 -0
- package/src/index.ts +70 -0
- package/tsconfig.json +10 -0
|
@@ -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();
|