@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,203 @@
1
+ import React, { useEffect, useState, useRef } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { createComponentService, createLibraryService, type InstalledComponent } from '@jlcpcb/core';
4
+ import open from 'open';
5
+ import { useNavigation, useCurrentScreen } from '../navigation/NavigationContext.js';
6
+ import type { InfoParams, ComponentInfo } from '../navigation/types.js';
7
+ import { useTerminalSize } from '../hooks/useTerminalSize.js';
8
+ import { DetailView, type DetailViewComponent } from '../components/DetailView.js';
9
+
10
+ const componentService = createComponentService();
11
+ const libraryService = createLibraryService();
12
+
13
+ export function InfoScreen() {
14
+ const { push } = useNavigation();
15
+ const { params } = useCurrentScreen() as { screen: 'info'; params: InfoParams };
16
+ const { columns: terminalWidth } = useTerminalSize();
17
+
18
+ const [component, setComponent] = useState<ComponentInfo | null>(
19
+ params.component || null
20
+ );
21
+ const [installedInfo, setInstalledInfo] = useState<InstalledComponent | null>(null);
22
+ const [libraryStatus, setLibraryStatus] = useState<Awaited<ReturnType<typeof libraryService.getStatus>> | null>(null);
23
+ const [isLoading, setIsLoading] = useState(!params.component);
24
+ const [error, setError] = useState<string | null>(null);
25
+ const [isCheckingLibrary, setIsCheckingLibrary] = useState(false);
26
+ const checkingRef = useRef(false);
27
+
28
+ // Check if component is installed and fetch full details
29
+ useEffect(() => {
30
+ const componentId = params.componentId;
31
+ if (!componentId) return;
32
+
33
+ const fetchData = async () => {
34
+ setIsLoading(true);
35
+ try {
36
+ // Fetch library status (for paths)
37
+ const status = await libraryService.getStatus();
38
+ setLibraryStatus(status);
39
+
40
+ // Check if already installed
41
+ const installed = await libraryService.listInstalled({});
42
+ const found = installed.find(c => c.lcscId === componentId);
43
+ if (found) {
44
+ setInstalledInfo(found);
45
+ }
46
+
47
+ // Fetch full details from API (always, to get price/stock/attributes)
48
+ const searchResults = await componentService.search(componentId, { limit: 1 });
49
+ if (searchResults.length > 0) {
50
+ setComponent(searchResults[0]);
51
+ } else if (!params.component) {
52
+ // Fallback to getDetails if search didn't work
53
+ const details = await componentService.getDetails(componentId);
54
+ if (details) {
55
+ setComponent(details);
56
+ } else {
57
+ setError('Component not found');
58
+ }
59
+ }
60
+ } catch (err) {
61
+ if (!params.component) {
62
+ setError(err instanceof Error ? err.message : 'Failed to fetch component');
63
+ }
64
+ } finally {
65
+ setIsLoading(false);
66
+ }
67
+ };
68
+
69
+ fetchData();
70
+ }, [params.componentId, params.component]);
71
+
72
+ // Get datasheet URL (different field names in different types)
73
+ const datasheetUrl = component && ('datasheetPdf' in component ? component.datasheetPdf : 'datasheet' in component ? component.datasheet : undefined);
74
+
75
+ const [isRegenerating, setIsRegenerating] = useState(false);
76
+ const [regenerateMessage, setRegenerateMessage] = useState<string | null>(null);
77
+
78
+ useInput((input, key) => {
79
+ if (isLoading || !component || isCheckingLibrary || isRegenerating) return;
80
+
81
+ const lowerInput = input.toLowerCase();
82
+
83
+ // S - Open Symbol in KiCad
84
+ if (lowerInput === 's' && installedInfo && libraryStatus) {
85
+ const symbolPath = `${libraryStatus.paths.symbolsDir}/JLC-MCP-${installedInfo.category}.kicad_sym`;
86
+ open(symbolPath);
87
+ return;
88
+ }
89
+
90
+ // F - Open Footprint in KiCad
91
+ if (lowerInput === 'f' && installedInfo && libraryStatus) {
92
+ if (installedInfo.footprintRef?.startsWith('JLC-MCP:')) {
93
+ const fpName = installedInfo.footprintRef.split(':')[1];
94
+ const footprintPath = `${libraryStatus.paths.footprintsDir}/JLC-MCP.pretty/${fpName}.kicad_mod`;
95
+ open(footprintPath);
96
+ }
97
+ // Standard KiCad footprints can't be opened directly
98
+ return;
99
+ }
100
+
101
+ // M - Open 3D Model
102
+ if (lowerInput === 'm' && installedInfo && libraryStatus && installedInfo.has3dModel) {
103
+ const modelPath = `${libraryStatus.paths.models3dDir}/${installedInfo.name}.step`;
104
+ open(modelPath);
105
+ return;
106
+ }
107
+
108
+ // R - Regenerate symbol and footprint
109
+ if (lowerInput === 'r' && installedInfo) {
110
+ setIsRegenerating(true);
111
+ setRegenerateMessage('Regenerating symbol and footprint...');
112
+
113
+ libraryService.install(component.lcscId, { force: true })
114
+ .then((result) => {
115
+ setRegenerateMessage(`✓ Regenerated: ${result.symbolAction}`);
116
+ // Clear message after 2 seconds
117
+ setTimeout(() => setRegenerateMessage(null), 2000);
118
+ })
119
+ .catch((err) => {
120
+ setRegenerateMessage(`✗ Failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
121
+ setTimeout(() => setRegenerateMessage(null), 3000);
122
+ })
123
+ .finally(() => setIsRegenerating(false));
124
+ return;
125
+ }
126
+
127
+ // D - Open Datasheet
128
+ if (lowerInput === 'd' && datasheetUrl) {
129
+ open(datasheetUrl);
130
+ return;
131
+ }
132
+
133
+ // Enter - Install (only when not installed)
134
+ if (key.return && !installedInfo) {
135
+ if (checkingRef.current) return;
136
+ checkingRef.current = true;
137
+ setIsCheckingLibrary(true);
138
+
139
+ if (libraryStatus && (!libraryStatus.installed || !libraryStatus.linked)) {
140
+ // Libraries not set up - show setup screen
141
+ push('library-setup', {
142
+ componentId: component.lcscId,
143
+ component,
144
+ });
145
+ setIsCheckingLibrary(false);
146
+ checkingRef.current = false;
147
+ } else {
148
+ // Libraries ready - proceed to install
149
+ push('install', {
150
+ componentId: component.lcscId,
151
+ component,
152
+ });
153
+ setIsCheckingLibrary(false);
154
+ checkingRef.current = false;
155
+ }
156
+ }
157
+ });
158
+
159
+ if (isLoading) {
160
+ return (
161
+ <Box flexDirection="column">
162
+ <Text color="yellow">⏳ Loading component {params.componentId}...</Text>
163
+ </Box>
164
+ );
165
+ }
166
+
167
+ if (isCheckingLibrary) {
168
+ return (
169
+ <Box flexDirection="column">
170
+ <Text color="yellow">Checking library status...</Text>
171
+ </Box>
172
+ );
173
+ }
174
+
175
+ if (error || !component) {
176
+ return (
177
+ <Box flexDirection="column">
178
+ <Text color="red">✗ {error || 'Component not found'}</Text>
179
+ <Text dimColor>Press Esc to go back</Text>
180
+ </Box>
181
+ );
182
+ }
183
+
184
+ return (
185
+ <Box flexDirection="column">
186
+ <Box marginBottom={1}>
187
+ <Text bold>
188
+ Component: <Text color="cyan">{component.lcscId}</Text>
189
+ {' '}
190
+ <Text dimColor>({component.name})</Text>
191
+ {installedInfo && <Text color="green"> ✓ Installed</Text>}
192
+ </Text>
193
+ </Box>
194
+ <DetailView
195
+ component={component}
196
+ terminalWidth={terminalWidth}
197
+ isInstalled={!!installedInfo}
198
+ installedInfo={installedInfo}
199
+ statusMessage={regenerateMessage}
200
+ />
201
+ </Box>
202
+ );
203
+ }
@@ -0,0 +1,54 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { createLibraryService, type InstallResult } from '@jlcpcb/core';
4
+ import { useNavigation, useCurrentScreen } from '../navigation/NavigationContext.js';
5
+ import type { InstallParams } from '../navigation/types.js';
6
+
7
+ const libraryService = createLibraryService();
8
+
9
+ export function InstallScreen() {
10
+ const { replace } = useNavigation();
11
+ const { params } = useCurrentScreen() as { screen: 'install'; params: InstallParams };
12
+
13
+ const [status, setStatus] = useState<'installing' | 'done'>('installing');
14
+
15
+ useEffect(() => {
16
+ let mounted = true;
17
+
18
+ async function install() {
19
+ try {
20
+ const result: InstallResult = await libraryService.install(params.componentId, {});
21
+ if (mounted) {
22
+ replace('installed', {
23
+ componentId: params.componentId,
24
+ component: params.component,
25
+ result,
26
+ });
27
+ }
28
+ } catch (err) {
29
+ if (mounted) {
30
+ replace('installed', {
31
+ componentId: params.componentId,
32
+ component: params.component,
33
+ error: err instanceof Error ? err.message : 'Unknown error',
34
+ });
35
+ }
36
+ }
37
+ }
38
+
39
+ install();
40
+
41
+ return () => {
42
+ mounted = false;
43
+ };
44
+ }, [params.componentId, params.component, replace]);
45
+
46
+ return (
47
+ <Box flexDirection="column">
48
+ <Text color="yellow">⏳ Installing {params.componentId}...</Text>
49
+ <Box marginTop={1}>
50
+ <Text dimColor>Fetching from EasyEDA and converting to KiCad format...</Text>
51
+ </Box>
52
+ </Box>
53
+ );
54
+ }
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { useNavigation, useCurrentScreen } from '../navigation/NavigationContext.js';
4
+ import type { InstalledParams } from '../navigation/types.js';
5
+ import { useTerminalSize } from '../hooks/useTerminalSize.js';
6
+ import { InstalledView } from '../components/InstalledView.js';
7
+
8
+ export function InstalledScreen() {
9
+ const { pop } = useNavigation();
10
+ const { params } = useCurrentScreen() as { screen: 'installed'; params: InstalledParams };
11
+ const { columns: terminalWidth } = useTerminalSize();
12
+
13
+ useInput(() => {
14
+ pop();
15
+ });
16
+
17
+ const result = params.result
18
+ ? {
19
+ symbolRef: params.result.symbolRef || `JLC:${params.component.name}`,
20
+ footprintRef: params.result.footprintRef || `JLC:${params.component.name}`,
21
+ }
22
+ : null;
23
+
24
+ return (
25
+ <Box flexDirection="column">
26
+ <Box marginBottom={1}>
27
+ <Text bold>
28
+ Install: <Text color="cyan">{params.componentId}</Text>
29
+ </Text>
30
+ </Box>
31
+ <InstalledView
32
+ component={params.component}
33
+ result={result}
34
+ error={params.error || null}
35
+ terminalWidth={terminalWidth}
36
+ />
37
+ </Box>
38
+ );
39
+ }
@@ -0,0 +1,259 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { createLibraryService, createComponentService, type InstalledComponent, type LibraryStatus } from '@jlcpcb/core';
4
+ import { useNavigation, useCurrentScreen } from '../navigation/NavigationContext.js';
5
+ import type { LibraryParams } from '../navigation/types.js';
6
+ import { useAppState } from '../state/AppStateContext.js';
7
+ import { useTerminalSize } from '../hooks/useTerminalSize.js';
8
+ import { Divider } from '../components/Divider.js';
9
+
10
+ const libraryService = createLibraryService();
11
+ const componentService = createComponentService();
12
+
13
+ function truncate(str: string, len: number): string {
14
+ if (!str) return '';
15
+ return str.length > len ? str.slice(0, len - 1) + '…' : str;
16
+ }
17
+
18
+ function StatusBadge({ installed, linked }: { installed: boolean; linked: boolean }) {
19
+ if (installed && linked) {
20
+ return <Text color="green">Installed & Linked</Text>;
21
+ } else if (installed && !linked) {
22
+ return <Text color="yellow">Installed (not linked)</Text>;
23
+ } else {
24
+ return <Text color="red">Not Installed</Text>;
25
+ }
26
+ }
27
+
28
+ export function LibraryScreen() {
29
+ const { push } = useNavigation();
30
+ const { params } = useCurrentScreen() as { screen: 'library'; params: LibraryParams };
31
+ const { selectedIndex, setSelectedIndex } = useAppState();
32
+ const { columns: terminalWidth } = useTerminalSize();
33
+
34
+ const [status, setStatus] = useState<LibraryStatus | null>(params.status || null);
35
+ const [installed, setInstalled] = useState<InstalledComponent[]>(params.installed || []);
36
+ const [descriptions, setDescriptions] = useState<Record<string, string>>({});
37
+ const [isLoading, setIsLoading] = useState(!params.status);
38
+ const [isSettingUp, setIsSettingUp] = useState(false);
39
+
40
+ useEffect(() => {
41
+ if (!params.status) {
42
+ setIsLoading(true);
43
+ Promise.all([libraryService.getStatus(), libraryService.listInstalled({})])
44
+ .then(([statusResult, installedResult]) => {
45
+ setStatus(statusResult);
46
+ setInstalled(installedResult);
47
+ })
48
+ .catch(() => {
49
+ setStatus(null);
50
+ setInstalled([]);
51
+ })
52
+ .finally(() => setIsLoading(false));
53
+ }
54
+ }, [params.status, params.installed]);
55
+
56
+ // Fetch descriptions for installed components
57
+ useEffect(() => {
58
+ if (installed.length === 0) return;
59
+
60
+ const fetchDescriptions = async () => {
61
+ const newDescriptions: Record<string, string> = {};
62
+
63
+ // Fetch in batches of 5 to avoid overwhelming the API
64
+ for (let i = 0; i < installed.length; i += 5) {
65
+ const batch = installed.slice(i, i + 5);
66
+ const results = await Promise.allSettled(
67
+ batch.map(async (item) => {
68
+ const details = await componentService.search(item.lcscId, { limit: 1 });
69
+ return { id: item.lcscId, description: details[0]?.description || '' };
70
+ })
71
+ );
72
+
73
+ for (const result of results) {
74
+ if (result.status === 'fulfilled' && result.value.description) {
75
+ newDescriptions[result.value.id] = result.value.description;
76
+ }
77
+ }
78
+
79
+ // Update state incrementally
80
+ setDescriptions(prev => ({ ...prev, ...newDescriptions }));
81
+ }
82
+ };
83
+
84
+ fetchDescriptions();
85
+ }, [installed]);
86
+
87
+ useInput((input, key) => {
88
+ if (isLoading || isSettingUp) return;
89
+
90
+ // If not installed, Enter triggers setup
91
+ if (status && (!status.installed || !status.linked)) {
92
+ if (key.return) {
93
+ setIsSettingUp(true);
94
+ libraryService
95
+ .ensureGlobalTables()
96
+ .then(() => libraryService.getStatus())
97
+ .then((newStatus) => {
98
+ setStatus(newStatus);
99
+ setIsSettingUp(false);
100
+ })
101
+ .catch(() => setIsSettingUp(false));
102
+ }
103
+ return;
104
+ }
105
+
106
+ if (key.upArrow) {
107
+ setSelectedIndex(Math.max(0, selectedIndex - 1));
108
+ } else if (key.downArrow) {
109
+ setSelectedIndex(Math.min(installed.length - 1, selectedIndex + 1));
110
+ } else if (key.return && installed[selectedIndex]) {
111
+ push('info', {
112
+ componentId: installed[selectedIndex].lcscId,
113
+ });
114
+ }
115
+ });
116
+
117
+ if (isLoading) {
118
+ return (
119
+ <Box flexDirection="column">
120
+ <Text color="yellow">Loading library status...</Text>
121
+ </Box>
122
+ );
123
+ }
124
+
125
+ if (isSettingUp) {
126
+ return (
127
+ <Box flexDirection="column">
128
+ <Text color="yellow">Setting up JLC-MCP libraries...</Text>
129
+ <Box marginTop={1}>
130
+ <Text dimColor>Creating directories and registering with KiCad...</Text>
131
+ </Box>
132
+ </Box>
133
+ );
134
+ }
135
+
136
+ // Not installed state
137
+ if (!status || !status.installed || !status.linked) {
138
+ return (
139
+ <Box flexDirection="column">
140
+ <Box marginBottom={1}>
141
+ <Text bold>JLC-MCP Libraries</Text>
142
+ </Box>
143
+ <Box marginBottom={1}>
144
+ <Text>Status: </Text>
145
+ <StatusBadge installed={status?.installed ?? false} linked={status?.linked ?? false} />
146
+ {status && <Text dimColor> (KiCad {status.version})</Text>}
147
+ </Box>
148
+ <Box marginTop={1} flexDirection="column">
149
+ <Text dimColor>The JLC-MCP libraries have not been set up yet.</Text>
150
+ <Text dimColor>This is required to install components.</Text>
151
+ </Box>
152
+ <Box marginTop={1} flexDirection="column">
153
+ <Divider width={terminalWidth} />
154
+ <Text>Press <Text color="cyan">Enter</Text> to set up libraries now</Text>
155
+ <Text dimColor>Esc Exit</Text>
156
+ <Divider width={terminalWidth} />
157
+ </Box>
158
+ </Box>
159
+ );
160
+ }
161
+
162
+ // Installed but empty state
163
+ if (installed.length === 0) {
164
+ return (
165
+ <Box flexDirection="column">
166
+ <Box marginBottom={1}>
167
+ <Text bold>JLC-MCP Libraries</Text>
168
+ </Box>
169
+ <Box>
170
+ <Text>Status: </Text>
171
+ <StatusBadge installed={status.installed} linked={status.linked} />
172
+ <Text dimColor> (KiCad {status.version})</Text>
173
+ </Box>
174
+ <Box>
175
+ <Text dimColor>Installed: 0 Components</Text>
176
+ </Box>
177
+ <Box marginBottom={1}>
178
+ <Text dimColor>Location: {status.paths.symbolsDir.replace(process.env.HOME || '', '~')}</Text>
179
+ </Box>
180
+ <Divider width={terminalWidth} />
181
+ <Box marginTop={1}>
182
+ <Text dimColor>No components installed yet. Use 'jlc search' to find and install components.</Text>
183
+ </Box>
184
+ <Box marginTop={1}>
185
+ <Divider width={terminalWidth} />
186
+ <Text dimColor>Esc Exit</Text>
187
+ <Divider width={terminalWidth} />
188
+ </Box>
189
+ </Box>
190
+ );
191
+ }
192
+
193
+ // Components table - full width responsive layout
194
+ // Fixed columns: selector(2) + name + category + status columns(15)
195
+ // Description takes remaining space
196
+ const nameWidth = 20;
197
+ const categoryWidth = 12;
198
+ const statusWidth = 15; // Sym(5) + FP(5) + 3D(5)
199
+ const descWidth = Math.max(terminalWidth - 2 - nameWidth - categoryWidth - statusWidth, 15);
200
+
201
+ return (
202
+ <Box flexDirection="column" width="100%">
203
+ <Box marginBottom={1}>
204
+ <Text bold>JLC-MCP Libraries</Text>
205
+ </Box>
206
+ <Box>
207
+ <Text>Status: </Text>
208
+ <StatusBadge installed={status.installed} linked={status.linked} />
209
+ <Text dimColor> (KiCad {status.version})</Text>
210
+ </Box>
211
+ <Box>
212
+ <Text dimColor>Installed: {installed.length} Component{installed.length !== 1 ? 's' : ''}</Text>
213
+ </Box>
214
+ <Box marginBottom={1}>
215
+ <Text dimColor>Location: {status.paths.symbolsDir.replace(process.env.HOME || '', '~')}</Text>
216
+ </Box>
217
+ <Divider width={terminalWidth} />
218
+ <Box marginBottom={1} marginTop={1}>
219
+ <Text bold dimColor>
220
+ {' '}
221
+ {'Name'.padEnd(nameWidth)}
222
+ {'Category'.padEnd(categoryWidth)}
223
+ {'Description'.padEnd(descWidth)}
224
+ {'Sym'.padEnd(5)}
225
+ {'FP'.padEnd(5)}
226
+ {'3D'}
227
+ </Text>
228
+ </Box>
229
+ {installed.map((item, i) => {
230
+ const isSelected = i === selectedIndex;
231
+ const desc = descriptions[item.lcscId] || '';
232
+ // Check if footprint is standard KiCad (not JLC-MCP custom)
233
+ const isStandardFp = item.footprintRef && !item.footprintRef.startsWith('JLC-MCP:');
234
+ const fpLabel = !item.footprintRef ? 'N' : isStandardFp ? 'S' : 'Y';
235
+ const fpColor = !item.footprintRef ? 'red' : isStandardFp ? 'cyan' : 'green';
236
+ return (
237
+ <Box key={`${item.lcscId}-${i}`}>
238
+ <Text color={isSelected ? 'cyan' : undefined}>
239
+ {isSelected ? '> ' : ' '}
240
+ </Text>
241
+ <Text inverse={isSelected}>
242
+ <Text color="cyan">{truncate(item.name, nameWidth - 1).padEnd(nameWidth)}</Text>
243
+ {truncate(item.category, categoryWidth - 1).padEnd(categoryWidth)}
244
+ <Text dimColor>{truncate(desc, descWidth - 1).padEnd(descWidth)}</Text>
245
+ <Text color="green">{'Y'.padEnd(5)}</Text>
246
+ <Text color={fpColor}>{fpLabel.padEnd(5)}</Text>
247
+ <Text color={item.has3dModel ? 'green' : 'red'}>{item.has3dModel ? 'Y' : 'N'}</Text>
248
+ </Text>
249
+ </Box>
250
+ );
251
+ })}
252
+ <Box marginTop={1} flexDirection="column">
253
+ <Divider width={terminalWidth} />
254
+ <Text dimColor>↑/↓ Navigate • Enter View Details • Esc Exit</Text>
255
+ <Divider width={terminalWidth} />
256
+ </Box>
257
+ </Box>
258
+ );
259
+ }
@@ -0,0 +1,120 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { createLibraryService } from '@jlcpcb/core';
4
+ import { useNavigation, useCurrentScreen } from '../navigation/NavigationContext.js';
5
+ import type { LibrarySetupParams } from '../navigation/types.js';
6
+ import { useTerminalSize } from '../hooks/useTerminalSize.js';
7
+ import { Divider } from '../components/Divider.js';
8
+
9
+ const libraryService = createLibraryService();
10
+
11
+ export function LibrarySetupScreen() {
12
+ const { push, pop } = useNavigation();
13
+ const { params } = useCurrentScreen() as { screen: 'library-setup'; params: LibrarySetupParams };
14
+ const { columns: terminalWidth } = useTerminalSize();
15
+
16
+ const [selectedOption, setSelectedOption] = useState<'install' | 'cancel'>('install');
17
+ const [isInstalling, setIsInstalling] = useState(false);
18
+ const [error, setError] = useState<string | null>(null);
19
+
20
+ useInput((input, key) => {
21
+ if (isInstalling) return;
22
+
23
+ if (key.leftArrow || key.rightArrow || input === 'h' || input === 'l') {
24
+ setSelectedOption(selectedOption === 'install' ? 'cancel' : 'install');
25
+ } else if (key.return) {
26
+ if (selectedOption === 'cancel') {
27
+ pop();
28
+ } else {
29
+ setIsInstalling(true);
30
+ setError(null);
31
+ libraryService
32
+ .ensureGlobalTables()
33
+ .then(() => {
34
+ // Success - continue to install the component
35
+ push('install', {
36
+ componentId: params.componentId,
37
+ component: params.component,
38
+ });
39
+ })
40
+ .catch((err) => {
41
+ setError(err instanceof Error ? err.message : 'Unknown error');
42
+ setIsInstalling(false);
43
+ });
44
+ }
45
+ } else if (key.escape) {
46
+ pop();
47
+ }
48
+ });
49
+
50
+ if (isInstalling) {
51
+ return (
52
+ <Box flexDirection="column">
53
+ <Box marginBottom={1}>
54
+ <Text bold color="yellow">Setting Up Libraries</Text>
55
+ </Box>
56
+ <Box marginTop={1}>
57
+ <Text color="yellow">Creating library directories...</Text>
58
+ </Box>
59
+ <Box marginTop={1}>
60
+ <Text dimColor>Registering with KiCad symbol and footprint tables...</Text>
61
+ </Box>
62
+ </Box>
63
+ );
64
+ }
65
+
66
+ return (
67
+ <Box flexDirection="column">
68
+ <Box marginBottom={1}>
69
+ <Text bold color="yellow">Library Setup Required</Text>
70
+ </Box>
71
+
72
+ <Box marginBottom={1}>
73
+ <Text>
74
+ JLC-MCP libraries need to be installed and linked to KiCad before you can install components.
75
+ </Text>
76
+ </Box>
77
+
78
+ <Box marginTop={1} flexDirection="column">
79
+ <Text dimColor>This will:</Text>
80
+ <Text dimColor> • Create library directories in ~/Documents/KiCad/*/3rdparty/</Text>
81
+ <Text dimColor> • Register libraries in KiCad's symbol and footprint tables</Text>
82
+ </Box>
83
+
84
+ {error && (
85
+ <Box marginTop={1}>
86
+ <Text color="red">Error: {error}</Text>
87
+ </Box>
88
+ )}
89
+
90
+ <Box marginTop={2}>
91
+ <Divider width={terminalWidth} />
92
+ </Box>
93
+
94
+ <Box marginTop={1} gap={2}>
95
+ <Text
96
+ inverse={selectedOption === 'install'}
97
+ color={selectedOption === 'install' ? 'green' : undefined}
98
+ >
99
+ {' Install Libraries '}
100
+ </Text>
101
+ <Text
102
+ inverse={selectedOption === 'cancel'}
103
+ color={selectedOption === 'cancel' ? 'red' : undefined}
104
+ >
105
+ {' Cancel '}
106
+ </Text>
107
+ </Box>
108
+
109
+ <Box marginTop={1}>
110
+ <Divider width={terminalWidth} />
111
+ </Box>
112
+ <Box>
113
+ <Text dimColor>←/→ Select • Enter Confirm • Esc Cancel</Text>
114
+ </Box>
115
+ <Box>
116
+ <Divider width={terminalWidth} />
117
+ </Box>
118
+ </Box>
119
+ );
120
+ }